We have been rejected by review team recently for a In-App Purchase issue.
We discovered one or more bugs in your app when reviewed on iPhone
running iOS 8.2 on both Wi-Fi and cellular networks.
Specifically,
your application’s In App Purchase Transaction cannot be completed.
The steps to reproduce are:
Launch app
Navigate to purchase/recharge tab
Proceed to purchase one of the In-App Purchase products
In-App Purchase confirmation window appears
Confirm purchase
In-App Purchase results in an error
BUT! In-App Purchase runs well in sandbox! We checked log on our server, but we can't find any receipt verification logs. It seems that purchase completion window didn't appear at all.
We found we would use the following code sometimes:
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];
Is that the ONLY reason? T.T
EDIT:
We had released several versions before. This time we added a new product. But we forgot to add its identifier to the following array.
-(void)requestProducts
{
if (_products) {
return;
}
NSSet* identifiers = [NSSet setWithObjects:
#"identifier",
#"new identifier", // forgot this line
nil];
SKProductsRequest* _request = [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers];
_request.delegate = self;
[_request start];
_requestProcessing = YES;
}
So the following code will return nil:
-(SKProduct*)getProduct:(NSString *)identifier
{
if (_products) {
for (SKProduct* product in _products) {
if ([product.productIdentifier isEqualToString:identifier]) {
return product;
}
}
} else if (!_requestProcessing) {
[self requestProducts];
}
return nil;
}
When user buy a product:
-(void)buyProductIdentifier:(NSString *)productIdentifier
{
if (![self isNetworkOK] || ![SKPaymentQueue canMakePayments]) {
[[NSNotificationCenter defaultCenter] postNotificationName:NotifyPurchaseFailed object:productIdentifier];
return;
}
NSLog(#"Buying %#...", productIdentifier);
SKProduct* product = [self getProduct:productIdentifier];
if (product) { // the new identifier gets nil here
SKPayment* payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
} else { // calls the deprecated api
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
BUT, it runs well in sandbox.
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed: // may break here
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
}
}
Any idea? All the products on itunes connect are in Approved state except the new one. The new product is in Waiting for Review state.
Related
I've read ALL the threads etc. on the internet and still am getting back zero restored transactions when I try to restore purchased products (sandbox environment).
I have created 3 Non-renewing products and 2 sandbox testers through iTunes connect.
I have had zero issues setting up the ability to actually purchase products; all is well.
I have created a "restore purchases" button and am trying to get it to work. I've been testing the following: install app and make purchase. Purchase again and it asks if I want to renew. Delete app and log out of app store. Reinstall app and press restore purchases ... zero!!!
Here's some code I'm using:
-(void)viewDidLoad
{
[super viewDidLoad];
//in-app purchase setup
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
-(void)viewDidAppear:(BOOL)animated
{
//make request for in-app products
if([SKPaymentQueue canMakePayments])
{
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObjects:#"product1_id",#"product2_id",#"product3_id",nil]];
request.delegate = self;
[request start];
}
else
{
//alert user
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"..." message:#"Please enable In App Purchase in settings" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
}
}
-(IBAction)restoreTransactionsPressed
{
_refreshRequest = [[SKReceiptRefreshRequest alloc] init];
_refreshRequest.delegate = self;
[_refreshRequest start];
}
-(void)requestDidFinish:(SKRequest *)request
{
if([request isKindOfClass:[SKReceiptRefreshRequest class]])
{
NSLog(#"Got a new receipt... %#",request.description);
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
}
-(void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
NSLog(#"%#",error.description);
}
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
NSLog(#"updatedTransactions");
for(SKPaymentTransaction *transaction in transactions)
{
NSLog(#"updatedTransaction:%# ... state:%#",transaction.originalTransaction.payment.productIdentifier,transaction.transactionState);
switch(transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
NSLog(#"SKPaymentTransactionStatePurchased");
//unlock feature code
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
NSLog(#"SKPaymentTransactionStateFailed");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
NSLog(#"SKPaymentTransactionStateRestored");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
default:
break;
}
}
}
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(#"paymentQueueRestoreCompletedTransactionsFinished");
NSLog(#"received restored transactions: %i", (int)queue.transactions.count);
for(SKPaymentTransaction *transaction in queue.transactions)
{
NSLog(#"transaction:%#",transaction);
}
}
-(void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
NSLog(#"restoreCompletedTransactionsFailedWithError %#",error);
}
Why do I always get back zero restored transactions?
it seems that "restoreCompletedTransactions" isn't in turn calling - paymentQueue:updatedTransactions: if that helps.
I'm unsure that SKReceiptRefreshRequest is necessary, but I've tried both refreshing the receipt before restoring and not refreshing the receipt before restoring.
Please don't post links to thread. I've investigated this for a few Days now! Some people claim that Apple's Sandbox had issues in the past ... could this be the reason? tyvm in advanced; all help is appreciated!
According to Apple's documentation, you can't restore non-renewing subscriptions. Also, have a read here for a more detailed explanation.
I am working on IAP first time. I have created test user and working good in sandbox env. I can see my Purchase and buy it as well(And of course i added StoreKit framework.), but problem is that: when i upload update with IAP to AppStore - moderator has reject my app, here is a reason:
We found that while you have submitted In App Purchase products for your app, the In App Purchase functionality is not present in your binary. Please see the attached screenshot/s for more information.(there is no some Purchase on the screen, empty space instead)
If you would like to utilize In App Purchase in your app, you will need to upload a new binary that incorporates the In App Purchase API to enable users to make a purchase
I just don't understand how it's possible? Why when i testing IAP - it's works fine, but it doesn't work when moderator doing it? All IAP have "Cleared for Sale" and "Waiting for review" status
Now if i upload app to the store will IAP works? Or maybe i need to do some additional action before?
Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
_iapArray = [[NSMutableArray alloc] init];
iap1 = #"com.mypurchase.addcoins1";
[self paymentCheck];
}
- (void)viewDidDisappear:(BOOL)animated
{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
- (void)paymentCheck{
if([SKPaymentQueue canMakePayments]){
NSLog(#"User can make payments");
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObjects:iap1, nil]];
productsRequest.delegate = self;
[productsRequest start];
}
else{
NSLog(#"User cannot make payments due to parental controls");
//this is called the user cannot make payments, most likely due to parental controls
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
SKProduct *validProduct = nil;
int count = [response.products count];
if(count > 0){
_iapArray = response.products;
indicator.hidden = YES;
[self.tableView reloadData];
}
else if(!validProduct){
NSLog(#"No products available");
UIAlertView *myAlertView = [[UIAlertView alloc] initWithTitle:#"Error"
message:#"Error"
delegate:nil cancelButtonTitle:#"OK" otherButtonTitles: nil];
[myAlertView show];
//this is called if your product id is not valid, this shouldn't be called unless that happens.
}
}
- (IBAction)purchase:(SKProduct *)product{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
for(SKPaymentTransaction *transaction in transactions){
switch (transaction.transactionState){
case SKPaymentTransactionStatePurchasing: NSLog(#"Transaction state -> Purchasing");
//called when the user is in the process of purchasing, do not add any of your own code here.
break;
case SKPaymentTransactionStatePurchased:
//this is called when the user has successfully purchased the package (Cha-Ching!)
// [self doRemoveAds]; //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(#"Transaction state -> Purchased - %#", transaction.payment.productIdentifier);
if ([transaction.payment.productIdentifier isEqualToString:iap1]) {
[self addCoins:5];
}
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Transaction state -> Restored");
//add the same code as you did from SKPaymentTransactionStatePurchased here
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
//called when the transaction does not finnish
if(transaction.error.code != SKErrorPaymentCancelled){
NSLog(#"Transaction state -> Cancelled");
//the user cancelled the payment ;(
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
As per their reply, you have integrated IAP in application but you have not used it. So, can you please make sure you have properly integrated IAP in application ? Please test in same Device and iOS if they mentioned.
I followed this tutorial to help me add in-app purchases. It works well, except now I am trying to add another non-consumable IAP. I defined my second product ID and created a method for the purchase button, but I'm not sure if I have to create another SKProductsRequest for this extra item, or if I use the same one, etc...
Here is my full code.
#define kRemoveAdsProductIdentifier #"099"
#define kDoubleEarningRateProductIdentifer #"199"
- (void)removeAds{
NSLog(#"User requests to remove ads");
if([SKPaymentQueue canMakePayments]){
NSLog(#"User can make payments");
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:kRemoveAdsProductIdentifier]];
productsRequest.delegate = self;
[productsRequest start];
}
else{
NSLog(#"User cannot make payments due to parental controls");
//this is called the user cannot make payments, most likely due to parental controls
}
}
- (void)doubleEarningRate{
NSLog(#"User requests 2x earning rate");
if([SKPaymentQueue canMakePayments]){
NSLog(#"User can make payments");
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:kDoubleEarningRateProductIdentifer]];
productsRequest.delegate = self;
[productsRequest start];
}
else{
NSLog(#"User cannot make payments due to parental controls");
//this is called the user cannot make payments, most likely due to parental controls
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
SKProduct *validProduct = nil;
int count = [response.products count];
if(count > 0){
validProduct = [response.products objectAtIndex:0];
NSLog(#"Products Available!");
[self purchase:validProduct];
}
else if(!validProduct){
NSLog(#"No products available");
//this is called if your product id is not valid, this shouldn't be called unless that happens.
}
}
- (void)purchase:(SKProduct *)product{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void) restore{
//this is called when the user restores purchases, you should hook this up to a button
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(#"received restored transactions: %i", queue.transactions.count);
for (SKPaymentTransaction *transaction in queue.transactions)
{
if(SKPaymentTransactionStateRestored){
NSLog(#"Transaction state -> Restored");
//called when the user successfully restores a purchase
[self doRemoveAds];
[self doubleEarningRate];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
for(SKPaymentTransaction *transaction in transactions){
switch (transaction.transactionState){
case SKPaymentTransactionStatePurchasing: NSLog(#"Transaction state -> Purchasing");
//called when the user is in the process of purchasing, do not add any of your own code here.
break;
case SKPaymentTransactionStatePurchased:
//this is called when the user has successfully purchased the package (Cha-Ching!)
[self doRemoveAds]; //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(#"Transaction state -> Purchased");
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Transaction state -> Restored");
//add the same code as you did from SKPaymentTransactionStatePurchased here
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
//called when the transaction does not finnish
if(transaction.error.code != SKErrorPaymentCancelled){
NSLog(#"Transaction state -> Cancelled");
//the user cancelled the payment ;(
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
-(void)doRemoveAds
{
areAdsRemoved = true;
NSLog(#"Ads Removed");
[topBanner removeFromSuperview];
}
-(void)doDoubleEarningRate
{
}
I also read through apple documentation which explained to me what each part does, but I am still clueless on how I can add another purchase, and most other tutorials are done differently or outdated. Also all the variables and indirection is a bit intimidating to me. So I am hoping that someone can give me a quick step by step guide for adding another purchase.
To make it clearer, this approach works perfectly for just one in app purchase. However I don't know how to add more, as in I don't know how to make the program recognize which in-app purchase is being selected.
Thanks.
I fixed this by creating a method called provideContent:(NSString *)productId. Then you can either use a switch statement or if statement to differentiate between product Id's. No need to create multiple types of productRequest methods, etc...
I used info from this brilliant page to add in-app purchases.
However I get the NSLog No Product Available. I have checked to make sure I have added the correct Product ID. I am at a loss to what to do and have been at this for several hours.
This is for a game built on Cocos2d and I can't figure out what is wrong. Sorry for the abundance of code.
I have added <SKProductsRequestDelegate, SKPaymentTransactionObserver> and <StoreKit/StoreKit.h>
.m
#define kRemoveAdsProductIdentifier #"COINS1000"
#implementation shopCoins
- (void)buy500Coins{
NSLog(#"User requests to remove ads");
if([SKPaymentQueue canMakePayments]){
NSLog(#"User can make payments");
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:kRemoveAdsProductIdentifier]];
productsRequest.delegate = self;
[productsRequest start];
}
else{
NSLog(#"User cannot make payments due to parental controls");
//this is called the user cannot make payments, most likely due to parental controls
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
SKProduct *validProduct = nil;
int count = [response.products count];
if(count > 0){
validProduct = [response.products objectAtIndex:0];
NSLog(#"Products Available!");
[self purchase:validProduct];
}
else if(!validProduct){
NSLog(#"No products available");
//this is called if your product id is not valid, this shouldn't be called unless that happens.
}
}
- (IBAction)purchase:(SKProduct *)product{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (IBAction) restore{
//this is called when the user restores purchases, you should hook this up to a button
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(#"received restored transactions: %i", queue.transactions.count);
for (SKPaymentTransaction *transaction in queue.transactions)
{
if(SKPaymentTransactionStateRestored){
NSLog(#"Transaction state -> Restored");
//called when the user successfully restores a purchase
[self doRemoveAds];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
for(SKPaymentTransaction *transaction in transactions){
switch (transaction.transactionState){
case SKPaymentTransactionStatePurchasing: NSLog(#"Transaction state -> Purchasing");
//called when the user is in the process of purchasing, do not add any of your own code here.
break;
case SKPaymentTransactionStatePurchased:
//this is called when the user has successfully purchased the package (Cha-Ching!)
[self doRemoveAds]; //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(#"Transaction state -> Purchased");
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Transaction state -> Restored");
//add the same code as you did from SKPaymentTransactionStatePurchased here
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
//called when the transaction does not finnish
if(transaction.error.code != SKErrorPaymentCancelled){
NSLog(#"Transaction state -> Cancelled");
//the user cancelled the payment ;(
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
- (void)doRemoveAds{
NSLog(#"Bitches");
}
If it's a debug build you probably need to add a sandbox user on iTunes connect this will allow you to use IAP that have not been reviewed yet. Note this user should match your iTunes connect account.
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
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.
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.