I've been experiencing various glitches with in-app purchases. Here are a few.
When I tap an item to buy it, it shows the Confirm you In-App Purchase alert and then after a second or so, it closes it self and opens the login alert.
Some times I'll log in, then it will ask me to log in again. Alerts pop up and go away on their own and it looks very glitchy. Some times the app crashes with no error message.
I haven't been able to search for any answers because these issues are quite strange.
Here is the code I'm using (I'm using a class named Game and a category names Game-Resources)
Game.m:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
product = response;
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
if (purchasing == NO){
// if I don't put (purchasing) in, this method is run multiples times
purchasing = YES;
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
for (SKPaymentTransaction * transaction in transactions){
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
NSLog(#"purchasing");
break;
case SKPaymentTransactionStatePurchased:
purchasing = NO;
NSLog(#"done");
if ([[NSUserDefaults standardUserDefaults] objectForKey:purchaseNSUserName] != nil){
int i = (int) [[NSUserDefaults standardUserDefaults] integerForKey:purchaseNSUserName];
[[NSUserDefaults standardUserDefaults] setInteger:i + purchaseNSUserAmount forKey:purchaseNSUserName];
[[NSUserDefaults standardUserDefaults] synchronize];
}else{
[[NSUserDefaults standardUserDefaults] setInteger:purchaseNSUserAmount forKey:purchaseNSUserName];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
purchasing = NO;
break;
case SKPaymentTransactionStateFailed:
purchasing = NO;
if (transaction.error.code != SKErrorPaymentCancelled){
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
//error happened
}
break;
case SKPaymentTransactionStateDeferred:
purchasing = NO;
break;
default:
break;
}
purchaseNSUserName = nil;
purchaseNSUserAmount = 0;
}
}
}
In the ViewDidLoad:
if ([SKPaymentQueue canMakePayments]){
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObjects:#"somethinghere", #"anotherthing", nil]];
self.productsRequest.delegate = self;
[self.productsRequest start];
}
In Game-Resources (when buy button is pressed):
SKPayment *payment = [SKPayment paymentWithProduct: product.products[productID]];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
Related
I have 2 issues :
After purchasing a subscription I receive the "You are all set Your purchase was successful" but in the SKPaymentTransactionObserver paymentQueue:updatedTransactions: I receive the SKPaymentTransactionStateFailed state with "Cannot connect to iTunes Store" error.
Some time when the purchase is initiated i receive a Confirm Purchase pop up but also the paymentQueue:updatedTransactions: is called with fail state
NOTE: I'm testing on simulator with Network Link Conditioner ( HighLatencyDNS custom profile 500ms)
Also I have notice this issue in the networks with high DNS latency more than 0.5 sec
Add Payment to queue:
- (BOOL)internalBeginPurchaseFor:(NSString *)productId
{
if ([SKPaymentQueue canMakePayments]) {
SKProduct *product;
if ((product = self.products[productId])) {
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
return YES;
}
}
return NO;
}
Processing transaction state:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
NSString *transactionIdentifier = transaction.transactionIdentifier;
NSString *transactionDetails = #"";
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
NSLog(#"User is purchasing");
transactionDetails = #"User purchasing";
break;
case SKPaymentTransactionStatePurchased:
NSLog(#"User purchased");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self executeHandlerForTransaction:transaction andSuccessFlag:YES];
transactionDetails = #"User purchased";
break;
case SKPaymentTransactionStateFailed:
NSLog(#"failed to purchase");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self executeHandlerForTransaction:transaction andSuccessFlag:NO];
transactionDetails = #"Failed to purchase";
break;
case SKPaymentTransactionStateRestored:
NSLog(#"restored transaction");
transactionDetails = #"Restored transaction";
break;
case SKPaymentTransactionStateDeferred:
NSLog(#"pending for transaction");
transactionDetails = #"Pending for transaction";
break;
}
NSString *stringData = [NSString stringWithFormat:#"%# for transaction Identifier %#",transactionDetails, transactionIdentifier];
[EventLogger logEventOfType:PURCHASE_PLAN withData:stringData andNotes:#"APIAPurchase"];
}
}
any suggestions ?
Hopefully this is enough information to help me solve my problem.
I am setting up In App Purchases via a templated source code that was given to me. In my MKStoreManager.m file, where I set up the featureID's for my in app purchases (Ex: static NSString *featureAId = #"com.logannat.myfirstgameTier1";)
When clicking on the actual button in the game, a method
- (void) buyFeature:(SKProduct*) product
{
if ([SKPaymentQueue canMakePayments])
{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
ends up getting called. I set a break point, and found the error to be in the last line of that code.
The variables that show up in the break point state are:
self = (MKStoreManager *)"reference number"
product = (SKProduct *) nil
and
payment = (SKPayment *) "reference number"
I can not seem to figure out what the problem is here. Any help would be much appreciated, thank you.
i do not have swift Right now so i am giving you Xcode code please convert the syntax accordingly its not so difficult.
so here is the complete life cycle for the in app purchased.
in your header file call these delegates and setup the bool for checking the status of you purchased
like that
#import <UIKit/UIKit.h>
BOOL arefavorite_access;
BOOL areAdsRemoved;
#interface ViewController : UIViewController<SKProductsRequestDelegate, SKPaymentTransactionObserver>{
}
- (IBAction)purchase;
- (IBAction)restore;
- (IBAction)tapsRemoveAds:(id)sender;
#end
one you need to hockup this tapsRemoveAds method on touchupinside of you button selector ant it will start working.
- (IBAction)tapsRemoveAds:(id)sender{
UIButton*btn=(UIButton*)sender;
NSString *featureAId = #"com.logannat.myfirstgameTier1";
NSLog(#"User requests to remove ads");
if([SKPaymentQueue canMakePayments]){
NSLog(#"User can make payments");
SKProductsRequest *productsRequest;
if (btn.tag==1) {
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject: featureAId]];
_protype= featureAId;
}
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
}
}
product request method
- (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{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
NSLog(#"%#",queue );
NSLog(#"Restored Transactions are once again in Queue for purchasing %#",[queue transactions]);
NSMutableArray *purchasedItemIDs = [[NSMutableArray alloc] init];
NSLog(#"received restored transactions: %i", queue.transactions.count);
for (SKPaymentTransaction *transaction in queue.transactions) {
NSString *productID = transaction.payment.productIdentifier;
[purchasedItemIDs addObject:productID];
NSLog (#"product id is %#" , productID);
_protype=productID;
[self doRemoveAds];
// here put an if/then statement to write files based on previously purchased items
// example if ([productID isEqualToString: #"youruniqueproductidentifier]){write files} else { nslog sorry}
}
if(queue.transactions.count==0){
[Utilities showOKAlertWithTitle:#"Restore purchase" message:#"you have no product available for restoration"];
}
}
- (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");
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Transaction state -> Restored");
_protype=transaction.payment.productIdentifier;
[self doRemoveAds];
//add the same code as you did from SKPaymentTransactionStatePurchased here
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
break;
case SKPaymentTransactionStateFailed:
//called when the transaction does not finish
if(transaction.error.code == SKErrorPaymentCancelled){
NSLog(#"Transaction state -> Cancelled");
//the user cancelled the payment ;(
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
break;
}
}
}
- (void)doRemoveAds{
// // [self.bannerView setAlpha:0];
NSString *featureAId = #"com.logannat.myfirstgameTier1";
NSString *featureAIdnumber2 = #"com.logannat.myfirstgameTier2";
if([_protype isEqualToString:featureAId]){
arefavorite_access = YES;
[[NSUserDefaults standardUserDefaults] setBool:arefavorite_access forKey:frareFavoriteRemoved];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else if([_protype isEqualToString: featureAIdnumber2]){
areAdsRemoved = YES;
[[NSUserDefaults standardUserDefaults] setBool:areAdsRemoved forKey:frareAdsRemoved];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
// [self showpopUp:self];
}
if this answer helps you please vote up and accept the answer
I have been following this tutorial on implementing an in app purchase - How do you add an in-app purchase to an iOS application?
However I get errors on lines:
productsRequest.delegate = self;
"Assigning to 'id <SKProductsRequestDelegate> from incompatible type "ViewController *const___strong'
and
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
Sending "ViewController *const___strong' to parameter of incompatible type 'id <SKPaymentTransactionObserver>
This is the .h:
#import <UIKit/UIKit.h>
#import <iAd/iAd.h>
#import <StoreKit/StoreKit.h>
#interface ViewController : UIViewController
{
IBOutlet UIButton *removeAdsButton;
}
- (IBAction)purchase;
- (IBAction)restore;
- (IBAction)tapsRemoveAdsButton;
#end
BOOL areAdsRemoved;
And this is my .m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//Removal of ads
areAdsRemoved = [[NSUserDefaults standardUserDefaults] boolForKey:#"areAddsRemoved"];
[[NSUserDefaults standardUserDefaults] synchronize];
//this will load wether or not they bought the in-app purchase
if(areAdsRemoved){
[self.view setBackgroundColor:[UIColor blueColor]];
//if they did buy it, set the background to blue, if your using the code above to set the background to blue, if your removing ads, your going to have to make your own code here
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#define kRemoveAdsProductIdentifier #"com.companyname.appname.removeads"
- (IBAction)tapsRemoveAdsButton {
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{
ADBannerView *banner;
[banner setAlpha:0];
areAdsRemoved = YES;
removeAdsButton.hidden = YES;
removeAdsButton.enabled = NO;
[[NSUserDefaults standardUserDefaults] setBool:areAdsRemoved forKey:#"areAdsRemoved"];
//use NSUserDefaults so that you can load wether or not they bought it
[[NSUserDefaults standardUserDefaults] synchronize];
}
#end
Those are not errors but warnings, you should add protocol conformance to your class declaration.
#interface ViewController : UIViewController < SKPaymentTransactionObserver, SKProductsRequestDelegate >
Be sure to implement every required method of both protocols.
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.
I have a nonconsumable IAP for a "Save Feature." It was recently rejected because I did not include a restore feature in the app. I added code for restoring, but when I created a new test user that hadn't purchased the app and pressed the restore button, the save feature was unlocked.
Here is how I check if a user has purchased the save feature
if (![userDefaults boolForKey:#"isPurchase"]){
[self purchaseMyProduct:[validProducts objectAtIndex:0]];
}
else { ... }
These are the methods I added to restore IAP's.
- (void)restorePurchases { //called when button pressed
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue]restoreCompletedTransactions];
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
for (SKPaymentTransaction *transaction in queue.transactions) {
NSString *productID = transaction.payment.productIdentifier;
if ([productID isEqualToString:#"facePuppetsSave"]) {
NSLog(#"called");
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setBool:YES forKey:#"isPurchase"];
[userDefaults synchronize];
}
}
}
Here are the methods I already had.
-(void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
break;
case SKPaymentTransactionStatePurchased:
if ([transaction.payment.productIdentifier
isEqualToString:#"facePuppetsSave"]) {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setBool:YES forKey:#"isPurchase"];
[userDefaults synchronize];
UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:
#"Purchase completed. Saving now available." message:nil delegate:
self cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alertView show];
[alertView release];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Restored ");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
break;
default:
break;
}
}
}
-(void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
{
SKProduct *validProduct = nil;
int count = [response.products count];
if (count>0) {
validProducts = response.products;
[validProducts retain];
validProduct = [response.products objectAtIndex:0];
}
Also, for the new test user example, I noticed that the NSLog "called" from the paymentQueueRestoreCompletedTransactionsFinished method is being called twice and "restored" from the paymentQueue:updatedTransactions: method is not being called at all.
How can I modify my code to restore properly?
Follow this:
#pragma mark SKPaymentTransactionOBserver
- (void)storeRestoreTransactionsFinished:(NSNotification*)notification
{
NSLog(#"- (void)storeRestoreTransactionsFinished:(NSNotification*)notification");
[apDelegate hideLoading];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
NSLog(#"Prossing.............");
break;
case SKPaymentTransactionStatePurchased:
{
[self completeTransaction:transaction];
NSError* error;
NSDictionary* jsonDict = [NSJSONSerialization
JSONObjectWithData:transaction.transactionReceipt
options:kNilOptions
error:&error];
NSLog(#"JSON Receipt: %#",jsonDict);
[[NSUserDefaults standardUserDefaults] setObject:jsonDict forKey:#"A"];
NSLog(#"Purchase was a Success.....");
}
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
NSLog(#"Purchase cancelled");
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
}
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
NSLog(#"restoreTransaction...");
[self validateReceiptForTransaction:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(#"Restore completed transactions finished.");
NSLog(#" Number of transactions in queue: %d", [[queue transactions] count]);
for (SKPaymentTransaction *trans in [queue transactions])
{
NSLog(#" transaction id %# for product %#.", [trans transactionIdentifier], [[trans payment] productIdentifier]);
NSLog(#" original transaction id: %# for product %#.", [[trans originalTransaction] transactionIdentifier],
[[[trans originalTransaction] payment]productIdentifier]);
if ([[[trans payment] productIdentifier] isEqual: kMDPulseSubscriptionProductIdentifier]) {
NSLog(#"Purchase Restored");
// Do your stuff to unlock
}
}
}
- (void)restoreCompletedTransactions
{
NSLog(#"Restore Tapped in transaction process");
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}