In App Purchase Crashes on [[SKPaymentQueue defaultQueue] addPayment:payment] - iphone

My In-App-Purchases work. I present a ModalView with a "Buy" UIButton. You click the button and the In App Purchase goes through the process. You can even do it several times in a row.
The problem occurs if you open the Modal View, then close the Modal View (using a UITabBarButtonItem), then reopen the Modal View and tap the "Buy" button. The app crashes and I get an NSZombie that reads
*** -[InAppPurchaseManager respondsToSelector:]: message sent to
deallocated instance 0x1c7ad0
The NSZombie points to line 160 in the .m file. I have marked it with comments.
I got the original code from this page: http://troybrant.net/blog/2010/01/in-app-purchases-a-full-walkthrough/
I have been struggling with this for many days now... any help would be awesome.
Here is the .h
//
// InAppPurchaseManager.h
// Copyright 2010 __MyCompanyName__. All rights reserved.
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
#define kInAppPurchaseManagerProductsFetchedNotification #"kInAppPurchaseManagerProductsFetchedNotification"
#define kInAppPurchaseManagerTransactionFailedNotification #"kInAppPurchaseManagerTransactionFailedNotification"
#define kInAppPurchaseManagerTransactionSucceededNotification #"kInAppPurchaseManagerTransactionSucceededNotification"
#define kInAppPurchaseCreditProductId #"com.myname.app.iap"
#interface InAppPurchaseManager : UIViewController <SKProductsRequestDelegate, SKPaymentTransactionObserver>
{
SKProduct *productID;
SKProductsRequest *productsRequest;
IBOutlet UIBarButtonItem *closeButton;
IBOutlet UIButton *buyButton;
IBOutlet UILabel *testLabel;
}
#property (retain, nonatomic) SKProduct *productID;
#property (retain, nonatomic) SKProductsRequest *productsRequest;
#property (retain, nonatomic) IBOutlet UIBarButtonItem *closeButton;
#property (retain, nonatomic) IBOutlet UIButton *buyButton;
#property (retain, nonatomic) IBOutlet UILabel *testLabel;
// public methods
-(void)loadStore;
-(BOOL)canMakePurchases;
-(void)purchaseCredit;
-(void)requestInAppPurchaseData;
-(void)buyButtonAction:(id)sender;
-(void)closeButtonAction:(id)sender;
-(void)updateButtonStatus:(NSString *)status;
#end
Here is the .m
// InAppPurchaseManager.m
#import "InAppPurchaseManager.h"
#implementation InAppPurchaseManager
#synthesize productID;
#synthesize productsRequest;
#synthesize closeButton;
#synthesize buyButton;
#synthesize testLabel;
- (void)dealloc {
[productID release];
//[productsRequest release];
[closeButton release];
[buyButton release];
[testLabel release];
[super dealloc];
}
- (void)viewDidLoad {
[super viewDidLoad];
[closeButton release];
closeButton = [[UIBarButtonItem alloc] initWithTitle:#"Close" style:UIBarButtonItemStyleBordered target:self action:#selector(closeButtonAction:)];
self.navigationItem.leftBarButtonItem = closeButton;
[self loadStore];
self.navigationItem.title = #"Credits";
}
-(void)closeButtonAction:(id)sender {
[self dismissModalViewControllerAnimated:YES];
}
-(void)buyButtonAction:(id)sender {
if([self canMakePurchases]) {
[self updateButtonStatus:#"OFF"];
[self performSelectorOnMainThread:#selector(requestInAppPurchaseData) withObject:nil waitUntilDone:NO];
} else {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:[NSString stringWithString:#"Your account settings do not allow for In App Purchases."] delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alertView show];
[alertView release];
}
}
-(void)updateButtonStatus:(NSString *)status {
if ([status isEqual:#"OFF"]) {
closeButton.enabled = NO;
buyButton.enabled = NO;
buyButton.titleLabel.textColor = [UIColor grayColor];
} else {
closeButton.enabled = YES;
buyButton.enabled = YES;
buyButton.titleLabel.textColor = [UIColor blueColor];
}
}
#pragma mark -
#pragma mark SKProductsRequestDelegate methods
//
// call this method once on startup
//
- (void)loadStore
{
// restarts any purchases if they were interrupted last time the app was open
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
- (void)requestInAppPurchaseData
{
NSSet *productIdentifiers = [NSSet setWithObject:kInAppPurchaseCreditProductId];
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
// we will release the request object in the delegate callback
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *products = response.products;
productID = [products count] == 1 ? [[products objectAtIndex:0] retain] : nil;
if (productID)
{
/*
NSLog(#"Product title: %#" , productID.localizedTitle);
NSLog(#"Product description: %#" , productID.localizedDescription);
NSLog(#"Product price: %#" , productID.price);
NSLog(#"Product id: %#" , productID.productIdentifier);
*/
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
NSString *currentCredits = ([standardUserDefaults objectForKey:#"currentCredits"]) ? [standardUserDefaults objectForKey:#"currentCredits"] : #"0";
testLabel.text = [NSString stringWithFormat:#"%#", currentCredits];
}
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
//NSLog(#"Invalid product id: %#" , invalidProductId);
testLabel.text = #"Try Again Later.";
}
// finally release the reqest we alloc/init’ed in requestProUpgradeProductData
[productsRequest release];
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
[self performSelectorOnMainThread:#selector(purchaseCredit) withObject:nil waitUntilDone:NO];
}
//
// call this before making a purchase
//
- (BOOL)canMakePurchases
{
return [SKPaymentQueue canMakePayments];
}
//
// kick off the upgrade transaction
//
- (void)purchaseCredit
{
SKPayment *payment = [SKPayment paymentWithProductIdentifier:kInAppPurchaseCreditProductId];
// *********************************************************************************************************
[[SKPaymentQueue defaultQueue] addPayment:payment]; // <--- This is where the NSZombie Appears *************
// *********************************************************************************************************
}
#pragma -
#pragma Purchase helpers
//
// saves a record of the transaction by storing the receipt to disk
//
- (void)recordTransaction:(SKPaymentTransaction *)transaction
{
if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseCreditProductId])
{
// save the transaction receipt to disk
[[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:#"InAppPurchaseTransactionReceipt" ];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
//
// enable pro features
//
- (void)provideContent:(NSString *)productId
{
if ([productId isEqualToString:kInAppPurchaseCreditProductId])
{
// Increment currentCredits
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
NSString *currentCredits = [standardUserDefaults objectForKey:#"currentCredits"];
int newCreditCount = [currentCredits intValue] + 1;
[standardUserDefaults setObject:[NSString stringWithFormat:#"%d", newCreditCount] forKey:#"currentCredits"];
testLabel.text = [NSString stringWithFormat:#"%d", newCreditCount];
}
}
//
// removes the transaction from the queue and posts a notification with the transaction result
//
- (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful
{
// remove the transaction from the payment queue.
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, #"transaction" , nil];
if (wasSuccessful)
{
// send out a notification that we’ve finished the transaction
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo];
}
else
{
// send out a notification for the failed transaction
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo];
}
[self updateButtonStatus:#"ON"];
}
//
// called when the transaction was successful
//
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
[self updateButtonStatus:#"OFF"];
[self recordTransaction:transaction];
[self provideContent:transaction.payment.productIdentifier];
[self finishTransaction:transaction wasSuccessful:YES];
}
//
// called when a transaction has been restored and and successfully completed
//
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
[self recordTransaction:transaction.originalTransaction];
[self provideContent:transaction.originalTransaction.payment.productIdentifier];
[self finishTransaction:transaction wasSuccessful:YES];
}
//
// called when a transaction has failed
//
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
if (transaction.error.code != SKErrorPaymentCancelled)
{
// error!
[self finishTransaction:transaction wasSuccessful:NO];
}
else
{
// this is fine, the user just cancelled, so don’t notify
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
[self updateButtonStatus:#"ON"];
}
#pragma mark -
#pragma mark SKPaymentTransactionObserver methods
//
// called when the transaction status is updated
//
- (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;
}
}
}
#end

The error message indicates a message is being sent to a deallocated instance of InAppPurchaseManager, which is your class. And it's happening after you open the view (creating an instance), close the view (releasing an instance), then opening the view again (creating a second instance). And the problem is happening within the addPayment: call. This indicates that the framework still has a handle on your old, released instance, and is trying to send it a message.
You give the framework a handle to your object in loadStore, when you call
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
I don't see anywhere where you remove self as an observer. Objects that send out notifications usually do not retain their observers, since doing so can create a retain cycle and/or a memory leak.
In your dealloc code you need to cleanup and call removeTransactionObserver:. That should solve your problem.

I think observers added using addTransactionObserver are apparently weak references - not strong ones, which would explain this. I've made a simple test:
// bad code below:
// the reference is weak so the observer is immediately destroyed
addTransactionObserver([[MyObserver alloc] init]);
...
[[SKPaymentQueue defaultQueue] addPayment:payment]; // crash
And got the same crash even without calling removeTransactionObserver.
The solution in my case was to simply keep a strong reference to the observer:
#property (strong) MyObserver* observer;
....
self.observer = [[MyObserver alloc] init];
addTransactionObserver(observer);

Related

Consumable product is acting strange and making duplicate requests.. What could be the cause?

I am doing in app purchase on two of my controllers say view1,view2.
when I buy a consumable product in view1 everything works fine.
but when I buy another consumable product on view2 after the transaction is complete it fires the old request I made on view1 and not the new view2 request. And vice versa
Also if I rebuy the same product from same controller after rebuying it makes the request which I made before it...
Example:
Controller1:
product1 > buy > someaction(say hit to the server with some params like "name= ABC")
user enters name and is dynamic
Again,
if I rebuy it
product1 > buy > someaction(say hit to the server with some params like "name= XYZ")
Now when product is bought it hits the server with old params "name=ABC" and not "name=XYZ" what user enters again..
product
**heres the code m using in my IAPhelper**
- (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);
}
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)recordTransaction:(SKPaymentTransaction *)transaction {
// TODO: Record the transaction on the server side...
}
- (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
{
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)buyProductIdentifier:(NSString *)productIdentifier {
NSLog(#"Buying in IAPHelper %#...", productIdentifier);
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[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
**heres the code m using in my view1controller**
-(void)viewDidAppear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(productPurchased:) name:kProductPurchasedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector: #selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(productsLoaded:) name:kProductsLoadedNotification object:nil];
Reachability *reach = [Reachability reachabilityForInternetConnection];
NetworkStatus netStatus = [reach currentReachabilityStatus];
if (netStatus == NotReachable) {
NSLog(#"No internet connection!");
} else {
if ([InAppRageIAPHelper sharedHelper].products == nil) {
[[InAppRageIAPHelper sharedHelper] requestProducts];
// self.hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
_hud.labelText = #"Loading comics...";
// [self performSelector:#selector(timeout:) withObject:nil afterDelay:30.0];
}
}
}
- (void)productPurchased:(NSNotification *)notification {
[self removeLoader];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
NSString *productIdentifier = (NSString *) notification.object;
NSLog(#"Purchased: %#", productIdentifier);
NSString *inaapstatus =[[NSUserDefaults standardUserDefaults] objectForKey:#"inaapstatus"];
if ([inaapstatus isEqualToString:#"addabeep"]) {
NSUserDefaults *inaapstatus = [NSUserDefaults standardUserDefaults];
[inaapstatus setValue:#"reset" forKey:#"inaapstatus"];
[[NSUserDefaults standardUserDefaults]synchronize];
if ([productIdentifier isEqualToString:#"com.beepbXXXXXXXX"]) {
duration = #"30";
NSUserDefaults *purchased = [NSUserDefaults standardUserDefaults];
[purchased setValue:#"YES" forKey:#"purchased"];
[[NSUserDefaults standardUserDefaults] synchronize];
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(purchased:) userInfo:nil repeats:NO];
}
else {
duration = #"7";
NSUserDefaults *purchased = [NSUserDefaults standardUserDefaults];
[purchased setValue:#"YES" forKey:#"purchased"];
[[NSUserDefaults standardUserDefaults] synchronize];
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(purchased:) userInfo:nil repeats:NO];
}
}
else
{
}
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(makeMyBeeps1Request:) userInfo:nil repeats:NO];
}
-(void)purchased:(NSTimer *)timer
{
NSString *purchased =[[NSUserDefaults standardUserDefaults] objectForKey:#"purchased" ];
if ([purchased isEqualToString:#"YES"]) {
[self doLogin];
NSUserDefaults *purchased1 = [NSUserDefaults standardUserDefaults];
[purchased1 setValue:#"NO" forKey:#"purchased"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void)productPurchaseFailed:(NSNotification *)notification {
[self removeLoader];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
// [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
SKPaymentTransaction * transaction = (SKPaymentTransaction *) notification.object;
if (transaction.error.code != SKErrorPaymentCancelled) {
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:#"Oops error occcured!"
message:transaction.error.localizedDescription
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:#"OK", nil] autorelease];
[alert show];
//[alert release];
}
}
- (void)dismissHUD:(id)arg {
NSLog(#"dismissHUD");
[self removeLoader];
}
- (void)productsLoaded:(NSNotification *)notification {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
// [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
[self removeLoader];
}
- (void)timeout:(id)arg {
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:#"Timeout!"
message:#"Please try again later."
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:#"OK", nil] autorelease];
[alert show];
// [alert release];
//[self performSelector:#selector(dismissHUD:) withObject:nil afterDelay:3.0];
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
//self.hud = nil;
// self.table.hidden = TRUE;
inappObserver = [[InAppPurchaseObserver alloc] init];
if ([SKPaymentQueue canMakePayments]) {
// Yes, In-App Purchase is enabled on this device!
// Proceed to fetch available In-App Purchase items.
// Replace "Your IAP Product ID" with your actual In-App Purchase Product ID,
// fetched from either a remote server or stored locally within your app.
SKProductsRequest *prodRequest= [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithObjects:#"com.beepccccceek1",#"com.beepbcccccnth1", nil ]];
prodRequest.delegate = self;
[prodRequest start];
} else {
// Notify user that In-App Purchase is disabled via button text.
[inappButton setTitle:#"In-App Purchase is Disabled" forState:UIControlStateNormal];
inappButton.enabled = NO;
}
[super viewDidLoad];
[self initializeView];
}
**view2 controller**
#pragma mark - View lifecycle
-(void)viewDidAppear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(productPurchased:) name:kProductPurchasedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector: #selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(productsLoaded:) name:kProductsLoadedNotification object:nil];
Reachability *reach = [Reachability reachabilityForInternetConnection];
NetworkStatus netStatus = [reach currentReachabilityStatus];
if (netStatus == NotReachable) {
NSLog(#"No internet connection!");
} else {
if ([InAppRageIAPHelper sharedHelper].products == nil) {
[[InAppRageIAPHelper sharedHelper] requestProducts];
// self.hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
[self performSelector:#selector(timeout:) withObject:nil afterDelay:30.0];
}
}
}
- (void)viewDidLoad
{
inappObserver = [[InAppPurchaseObserver alloc] init];
if ([SKPaymentQueue canMakePayments]) {
// Yes, In-App Purchase is enabled on this device!
// Proceed to fetch available In-App Purchase items.
// Replace "Your IAP Product ID" with your actual In-App Purchase Product ID,
// fetched from either a remote server or stored locally within your app.
SKProductsRequest *prodRequest= [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithObjects:#"com.beepbccccecc1",#"com.beepcccp.month1",#"com.beeccccep.free", nil ]];
prodRequest.delegate = self;
[prodRequest start];
} else {
// Notify user that In-App Purchase is disabled via button text.
[inappButton setTitle:#"In-App Purchase is Disabled" forState:UIControlStateNormal];
inappButton.enabled = NO;
}
[super viewDidLoad];
[self initializeView];
[[UIApplication sharedApplication] setStatusBarHidden:NO];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
self.mTableView = nil;
self.numberofbeeps = nil;
self.segmentedControl = nil;
}
- (void)productPurchased:(NSNotification *)notification {
[self removeLoader];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
NSString *productIdentifier = (NSString *) notification.object;
NSLog(#"Purchased: %#", productIdentifier);
NSString *inaapstatus =[[NSUserDefaults standardUserDefaults] objectForKey:#"inaapstatus"];
if ([inaapstatus isEqualToString:#"expired"]) {
NSUserDefaults *inaapstatus = [NSUserDefaults standardUserDefaults];
[inaapstatus setValue:#"reset" forKey:#"inaapstatus"];
[[NSUserDefaults standardUserDefaults]synchronize];
if ([productIdentifier isEqualToString:#"com.beepbccccnth1"]) {
duration = #"30";
NSUserDefaults *purchased = [NSUserDefaults standardUserDefaults];
[purchased setValue:#"YES" forKey:#"purchased"];
[[NSUserDefaults standardUserDefaults] synchronize];
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(purchased:) userInfo:nil repeats:NO];
}
else {
duration = #"7";
NSUserDefaults *purchased = [NSUserDefaults standardUserDefaults];
[purchased setValue:#"YES" forKey:#"purchased"];
[[NSUserDefaults standardUserDefaults] synchronize];
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(purchased:) userInfo:nil repeats:NO];
}
}
else
{
}
[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:#selector(makeMyBeeps1Request:) userInfo:nil repeats:NO];
}
-(void)purchased:(NSTimer *)timer
{
NSString *purchased =[[NSUserDefaults standardUserDefaults] objectForKey:#"purchased" ];
if ([purchased isEqualToString:#"YES"]) {
[self durationRequest];
NSUserDefaults *purchased = [NSUserDefaults standardUserDefaults];
[purchased setValue:#"NO" forKey:#"purchased"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void)productPurchaseFailed:(NSNotification *)notification {
[self removeLoader];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
// [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
SKPaymentTransaction * transaction = (SKPaymentTransaction *) notification.object;
if (transaction.error.code != SKErrorPaymentCancelled) {
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:#"Oops error occcured!"
message:transaction.error.localizedDescription
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:#"OK", nil] autorelease];
[alert show];
//[alert release];
}
}
- (void)dismissHUD:(id)arg {
NSLog(#"dismissHUD");
[self removeLoader];
}
- (void)productsLoaded:(NSNotification *)notification {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
// [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
[self removeLoader];
}
- (void)timeout:(id)arg {
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:#"Timeout!"
message:#"Please try again later."
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:#"OK", nil] autorelease];
[alert show];
// [alert release];
//[self performSelector:#selector(dismissHUD:) withObject:nil afterDelay:3.0];
}
Any help is appreciated thnx a lot guys
I don't recommend you having more than one active purchase delegate at a time. Try to deactivate the first delegate in view1 after all process succeeds, and then make the purchase in view2, see if that happens again
Sorry couldnt get time to update my answer.
I solved it by removingObserver of the notification posted just after the notification request completes...
Hope it helps someone..

iOS 4 bluetooth discovery doesn't work

I'm reading "beginning iPad application development", and at the bluetooth chapter i'm testing the code exactly as it appears at the book. The only difference is that the book was for 3.2 and I'm using XCODE 4 for iOS >4.0.
XCODE does not throw any error or warning, it builds correctly, but when testing at the iPhone it doesn't discover other devices.
What's wrong?
The viewController.h looks like:
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#interface pruebaBluetoothViewController : UIViewController
<GKSessionDelegate, GKPeerPickerControllerDelegate> {
GKSession *currentSession;
IBOutlet UITextField *txtMessage;
IBOutlet UIButton *connect;
IBOutlet UIButton *disconnect;
GKPeerPickerController *picker;
}
#property (nonatomic, retain) GKSession *currentSession;
#property (nonatomic, retain) UITextField *txtMessage;
#property (nonatomic, retain) UIButton *connect;
#property (nonatomic, retain) UIButton *disconnect;
-(IBAction) btnSend:(id) sender;
-(IBAction) btnConnect:(id) sender;
-(IBAction) btnDisconnect:(id) sender;
#end
While the .m looks like:
#import "pruebaBluetoothViewController.h"
#implementation pruebaBluetoothViewController
#synthesize currentSession;
#synthesize txtMessage;
#synthesize connect;
#synthesize disconnect;
- (void)viewDidLoad {
[connect setHidden:NO];
[disconnect setHidden:YES];
[super viewDidLoad];
}
-(IBAction) btnConnect:(id) sender {
picker = [[GKPeerPickerController alloc] init];
picker.delegate = self;
picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
[connect setHidden:YES];
[disconnect setHidden:NO];
[picker show];
}
- (void)peerPickerController:(GKPeerPickerController *)pk
didConnectPeer:(NSString *)peerID
toSession:(GKSession *) session {
self.currentSession = session;
session.delegate = self;
[session setDataReceiveHandler:self withContext:nil];
picker.delegate = nil;
[picker dismiss];
[picker autorelease];
}
- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)pk {
picker.delegate = nil;
[picker autorelease];
[connect setHidden:NO];
[disconnect setHidden:YES];
}
-(IBAction) btnDisconnect:(id) sender {
[self.currentSession disconnectFromAllPeers];
[self.currentSession release];
currentSession = nil;
[connect setHidden:NO];
[disconnect setHidden:YES];
}
- (void)session:(GKSession *)session
peer:(NSString *)peerID
didChangeState:(GKPeerConnectionState)state {
switch (state) {
case GKPeerStateConnected:
NSLog(#"connected");
break;
case GKPeerStateDisconnected:
NSLog(#"disconnected");
[self.currentSession release];
currentSession = nil;
[connect setHidden:NO];
[disconnect setHidden:YES];
break;
}
}
- (void)dealloc {
[txtMessage release];
[currentSession release];
[super dealloc];
}
- (void) mySendDataToPeers:(NSData *) data {
if (currentSession)
[self.currentSession sendDataToAllPeers:data
withDataMode:GKSendDataReliable
error:nil];
}
-(IBAction) btnSend:(id) sender {
//---convert an NSString object to NSData---
NSData* data;
NSString *str = [NSString stringWithString:txtMessage.text];
data = [str dataUsingEncoding: NSASCIIStringEncoding];
[self mySendDataToPeers:data];
}
- (void) receiveData:(NSData *)data
fromPeer:(NSString *)peer
inSession:(GKSession *)session
context:(void *)context {
//---convert the NSData to NSString---
NSString* str;
str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Data received"
message:str
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
looks like you missed the following Delegate Method:
- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type {
GKSession *session = [[GKSession alloc] initWithSessionID:kTankSessionID displayName:nil sessionMode:GKSessionModePeer];
return [session autorelease]; // peer picker retains a reference, so autorelease ours so we don't leak.
}
Hope this helps.

iPhone In App Purchase issue

I use In App Purchase in my application, but I have problem when I'm testing that. I have four consumable products. Information about that products I show in tableview. Sometimes when I click a button to buy some product I get a transaction state SKPaymentTransactionStateFailed in updatedTransaction function but transaction.error localizedFailureReason is always null.
Once I noticed that one transaction was updated two times (in updatedTransaction function transaction with the same transactionIdentifier comes, state of transaction is SKPaymentTransactionStatePurchased) - is then that product two times purchased?.
So I have no idea where is the problem. Please help me.
I use that class to manage In App Purchase:
#implementation InAppPurchaseManager
#synthesize upgradeProducts;
#synthesize productsRequest;
#synthesize delegate;
- (id) init
{
self = [super init];
if (!self) return nil;
if ([SKPaymentQueue canMakePayments]) {
[self loadStore];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
upgradeProducts = [[NSMutableArray alloc] init];
delegate = nil;
return self;
}
+ (InAppPurchaseManager *) sharedInstance
{
static InAppPurchaseManager *myInstance = nil;
if (nil == myInstance) {
myInstance = [[[self class] alloc] init];
}
return myInstance;
}
- (void) loadStore
{
NSSet *productsIdentifiers = [[NSSet alloc] initWithObjects:PRODUCT_1_ID, PRODUCT_2_ID, PRODUCT_3_ID, PRODUCT_4_ID, nil];
[self requestUpgradeProductsData:productsIdentifiers];
[productsIdentifiers release];
}
- (void) requestUpgradeProductsData:(NSSet *) productIdentifiers
{
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
}
- (void) productsRequest:(SKProductsRequest *) request didReceiveResponse:(SKProductsResponse *) response
{
[upgradeProducts removeAllObjects];
for (int i = 0; i < [response.products count]; i++) {
SKProduct *product = [response.products objectAtIndex:i];
UpgradeProduct *upgradeProduct = [[UpgradeProduct alloc] initWithProductID:product.productIdentifier];
upgradeProduct.title = product.localizedTitle;
upgradeProduct.description = product.localizedDescription;
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
NSString *price = [numberFormatter stringFromNumber:product.price];
[numberFormatter release];
upgradeProduct.price = price;
[self.upgradeProducts addObject:upgradeProduct];
[upgradeProduct release];
}
[productsRequest release];
if ([self.delegate respondsToSelector:#selector(didLoadStore:)])
[self.delegate didLoadStore:self.upgradeProducts];
}
+ (BOOL) canMakePurchases
{
if ([SKPaymentQueue canMakePayments])
return YES;
else {
[Global showAlertViewWithTitle:NSLocalizedString(#"Payment Error", #"Payment Error Alert Title")
message:NSLocalizedString(#"You are not authorized to purchase from AppStore", #"Payment Error Alert Text when user cannot make payments from store")];
return NO;
}
}
- (void) purchaseUpgrade:(NSString *) productIdentifier
{
if ([InAppPurchaseManager canMakePurchases]) {
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
- (void) recordTransaction:(SKPaymentTransaction *) transaction
{
[[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:#"upgradeTransactionReceipt" ];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void) finishTransaction:(SKPaymentTransaction *) transaction
{
[self paymentSucceeded:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void) paymentSucceeded:(SKPaymentTransaction *) transaction
{
// provide content here
if ([self.delegate respondsToSelector:#selector(didFinishPaymentTransaction)])
[self.delegate didFinishPaymentTransaction];
}
- (void) completeTransaction:(SKPaymentTransaction *) transaction
{
[self recordTransaction:transaction];
[self finishTransaction:transaction];
}
- (void) restoreTransaction:(SKPaymentTransaction *) transaction
{
[self recordTransaction:transaction.originalTransaction];
[self finishTransaction:transaction];
}
- (void) failedTransaction:(SKPaymentTransaction *) transaction
{
if (transaction.error.code != SKErrorPaymentCancelled) {
NSMutableString *messageToBeShown = [[NSMutableString alloc] init];
if ([transaction.error localizedFailureReason] != nil) {
[messageToBeShown setString:[NSString stringWithFormat:#"%# %#", NSLocalizedString(#"Reason:", #"Reason Text in alert when payment transaction failed"), [transaction.error localizedFailureReason]]];
if ([transaction.error localizedRecoverySuggestion] != nil)
[messageToBeShown appendFormat:#", %# %#", NSLocalizedString(#"You can try:", #"Text for sugesstion in alert when payment transaction failed"), [transaction.error localizedRecoverySuggestion]];
}
[Global showAlertViewWithTitle:NSLocalizedString(#"Unable to complete your purchase", #"Payment transaction failed alert title")
message:messageToBeShown];
[messageToBeShown release];
if ([self.delegate respondsToSelector:#selector(didFailedPaymentTransaction)])
[self.delegate didFailedPaymentTransaction];
} else {
if ([self.delegate respondsToSelector:#selector(didCancelPaymentTransaction)])
[self.delegate didCancelPaymentTransaction];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (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) request:(SKRequest *) request didFailWithError:(NSError *) error
{
[Global showAlertViewWithTitle:NSLocalizedString(#"Payment Error", #"Payment Error Alert Title")
message:[NSString stringWithFormat:#"%#, %#", NSLocalizedString(#"Could not contact App Store properly", #"Alert text when request did fail"),
[error localizedDescription]]];
}
- (void) dealloc
{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
[upgradeProducts release];
if (productsRequest)
productsRequest = nil;
[super dealloc];
}
#end
In AppDelegate in function didFinishLaunchingWithOptions I make that:
[InAppPurchaseManager sharedInstance];
In Purchase View when I click a button I make:
UpgradeProduct *selectedProduct = [self.faxProducts objectAtIndex:[purchaseButton.identifier intValue]];
if (selectedProduct) {
[[InAppPurchaseManager sharedInstance] purchaseUpgrade:selectedProduct.productID];
}
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
This code is wrong, you can't use more than once. You know, if you add the observer for twice , you will find one transaction was updated two times.
I have the same problem, observer always has 2 transaction. Even i remove [[SKPaymentQueue defaultQueue] addPayment:payment], it still have 1 transaction. so i suspect the "canMakePayments"
After i remove [SKPaymentQueue canMakePayments], the problem seem to solve.. not sure why but may be it solve yours
If you're just testing, just make sure you sign out from your personal account and create a new test account in iTunesConnect.
Strangely it would fail if I don't use a test user. The API is full of black magic with little explanation.
Make sure you use the AppID correctly. I had the same issue "User cancelled" and I was not calling the transaction with the correct ID both times. I automatically append the com.mycompany.product" prefix to my code, but in some cases, the com.mycompany.product was already appended. Rookie bug...
I got this error because I had the wrong product identifier.

Threading / UIActivityIndicatorView with In App Purchase

Let me first say, my In App Purchase works.
I have been struggling with the Activity Indicator / Threading for more than a week now. I am having real problems getting my spinner (UIActivityIndicatorView) to play nice within my InAppPurchase.m
I am using the same threading code in many other places and it works fine.
Is there something about how the IAP process works that causes problems with basic threading?
Right now, the spinner spins between the time you tap the buyButton and the first alert appears ("Do you want to buy? ..."), but no spinner after that.
Here is the .m File:
// InAppPurchaseManager.m
#import "InAppPurchaseManager.h"
#import "GANTracker.h"
#implementation InAppPurchaseManager
#synthesize productID;
#synthesize productsRequest;
#synthesize closeButton;
#synthesize buyButton;
#synthesize testLabel;
#synthesize pView;
#synthesize spinner;
#synthesize spinnerLabel;
- (void)dealloc {
[productID release];
//[productsRequest release];
[closeButton release];
[buyButton release];
[testLabel release];
[pView release];
[spinner release];
[spinnerLabel release];
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
[super dealloc];
}
- (void)viewDidLoad {
[super viewDidLoad];
NSError *error;
if (![[GANTracker sharedTracker] trackPageview:#"/in_app_purchase" withError:&error]) {
//NSLog(#"No GAN Tracking");
}
UIColor *backgroundColor = [UIColor colorWithRed:.6745 green:.1333 blue:.1333 alpha:1];
pView.backgroundColor = backgroundColor;
[closeButton release];
closeButton = [[UIBarButtonItem alloc] initWithTitle:#"Close" style:UIBarButtonItemStyleBordered target:self action:#selector(closeButtonAction:)];
self.navigationItem.leftBarButtonItem = closeButton;
// create the "Loading..." label
[spinnerLabel release];
//CGRectMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height)
spinnerLabel = [[UILabel alloc] initWithFrame:CGRectMake(35, 145, 250, 35)];
[spinnerLabel setText:#"Connecting to App Store... "];
[spinnerLabel setTextColor:[UIColor whiteColor]];
[spinnerLabel setBackgroundColor:[UIColor blackColor]];
[spinnerLabel setTextAlignment:UITextAlignmentRight];
[self.view addSubview:spinnerLabel];
spinnerLabel.hidden = YES;
// create the spinner
[spinner release];
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[spinner setCenter:CGPointMake(55,162)];
[self.view addSubview:spinner];
spinner.backgroundColor = [UIColor blackColor];
spinner.hidesWhenStopped = YES;
[spinner stopAnimating];
self.navigationItem.title = #"Credits";
//[self spinTheSpinner];
//[self loadStore];
[NSThread detachNewThreadSelector:#selector(loadStore) toTarget:self withObject:nil];
}
-(void)viewDidAppear:(BOOL)animated {
[self doneSpinning];
[self updateButtonStatus:#"ON"];
}
-(void)spinTheSpinner {
NSLog(#"In App Purchase.m == SpinTheSpiner");
//NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[spinner startAnimating];
spinnerLabel.hidden=NO;
//[self performSelectorOnMainThread:#selector(doneSpinning) withObject:nil waitUntilDone:NO];
//[pool release];
}
-(void)doneSpinning {
NSLog(#"In App Purchase.m == DoneSpinning");
spinnerLabel.hidden = YES;
[spinner stopAnimating];
}
-(void)closeButtonAction:(id)sender {
[self dismissModalViewControllerAnimated:YES];
}
-(void)buyButtonAction:(id)sender {
if([self canMakePurchases]) {
[self updateButtonStatus:#"OFF"];
[self spinTheSpinner];
//[self performSelectorOnMainThread:#selector(requestInAppPurchaseData) withObject:nil waitUntilDone:NO];
[NSThread detachNewThreadSelector:#selector(requestInAppPurchaseData) toTarget:self withObject:nil];
} else {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:[NSString stringWithString:#"Your account settings do not allow for In App Purchases."] delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alertView show];
[alertView release];
}
}
-(void)updateButtonStatus:(NSString *)status {
if ([status isEqual:#"OFF"]) {
closeButton.enabled = NO;
buyButton.enabled = NO;
buyButton.titleLabel.textColor = [UIColor grayColor];
} else {
closeButton.enabled = YES;
buyButton.enabled = YES;
buyButton.titleLabel.textColor = [UIColor blueColor];
}
}
#pragma mark -
#pragma mark SKProductsRequestDelegate methods
//
// call this method once on startup
//
- (void)loadStore
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(#"Load Store");
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
// restarts any purchases if they were interrupted last time the app was open
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[self doneSpinning];
[pool release];
}
- (void)requestInAppPurchaseData
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(#"Request In App Purchase Data");
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
NSSet *productIdentifiers = [NSSet setWithObject:kInAppPurchaseCreditProductId];
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
//[self doneSpinning];
[pool release];
// we will release the request object in the delegate callback
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSLog(#"did Receive Response");
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
NSArray *products = response.products;
productID = [products count] == 1 ? [[products objectAtIndex:0] retain] : nil;
if (productID)
{
/*
NSLog(#"Product title: %#" , productID.localizedTitle);
NSLog(#"Product description: %#" , productID.localizedDescription);
NSLog(#"Product price: %#" , productID.price);
NSLog(#"Product id: %#" , productID.productIdentifier);
*/
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
NSString *currentCredits = ([standardUserDefaults objectForKey:#"currentCredits"]) ? [standardUserDefaults objectForKey:#"currentCredits"] : #"0";
testLabel.text = [NSString stringWithFormat:#"%#", currentCredits];
}
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
//NSLog(#"Invalid product id: %#" , invalidProductId);
testLabel.text = #"Try Again Later.";
}
// finally release the reqest we alloc/init’ed in requestProUpgradeProductData
[productsRequest release];
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
//[self performSelectorOnMainThread:#selector(purchaseCredit) withObject:nil waitUntilDone:NO];
[self purchaseCredit];
}
//
// call this before making a purchase
//
- (BOOL)canMakePurchases
{
NSLog(#"Can Make Payments");
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
return [SKPaymentQueue canMakePayments];
}
//
// kick off the upgrade transaction
//
- (void)purchaseCredit
{
// REMOVED FOR PRIVACY
}
#pragma -
#pragma Purchase helpers
//
// saves a record of the transaction by storing the receipt to disk
//
- (void)recordTransaction:(SKPaymentTransaction *)transaction
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseCreditProductId])
{
// save the transaction receipt to disk
[[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:#"InAppPurchaseTransactionReceipt" ];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[pool release];
}
//
// enable pro features
//
- (void)provideContent:(NSString *)productId
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
if ([productId isEqualToString:kInAppPurchaseCreditProductId])
{
// Increment currentCredits
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
NSString *currentCredits = [standardUserDefaults objectForKey:#"currentCredits"];
int newCreditCount = [currentCredits intValue] + 1;
[standardUserDefaults setObject:[NSString stringWithFormat:#"%d", newCreditCount] forKey:#"currentCredits"];
testLabel.text = [NSString stringWithFormat:#"%d", newCreditCount];
}
[pool release];
}
//
// removes the transaction from the queue and posts a notification with the transaction result
//
- (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful
{
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
// remove the transaction from the payment queue.
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, #"transaction" , nil];
if (wasSuccessful)
{
// send out a notification that we’ve finished the transaction
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo];
}
else
{
// send out a notification for the failed transaction
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo];
}
[self updateButtonStatus:#"ON"];
}
//
// called when the transaction was successful
//
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
[self updateButtonStatus:#"OFF"];
[self spinTheSpinner];
[NSThread detachNewThreadSelector:#selector(recordTransaction:) toTarget:self withObject:transaction];
[NSThread detachNewThreadSelector:#selector(provideContent:) toTarget:self withObject:transaction.payment.productIdentifier];
//[self recordTransaction:transaction];
//[self provideContent:transaction.payment.productIdentifier];
[NSThread detachNewThreadSelector:#selector(threadFinishTransaction:) toTarget:self withObject:transaction];
//[self finishTransaction:transaction wasSuccessful:YES];
NSError *error;
if (![[GANTracker sharedTracker] trackPageview:#"/in_app_purchase_done" withError:&error]) {
//NSLog(#"No GAN Tracking");
}
[self doneSpinning];
}
-(void)threadFinishTransaction:(SKPaymentTransaction *)transaction {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self finishTransaction:transaction wasSuccessful:YES];
[pool release];
}
//
// called when a transaction has been restored and and successfully completed
//
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
[self recordTransaction:transaction.originalTransaction];
[self provideContent:transaction.originalTransaction.payment.productIdentifier];
[self finishTransaction:transaction wasSuccessful:YES];
}
//
// called when a transaction has failed
//
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
if (transaction.error.code != SKErrorPaymentCancelled)
{
// error!
NSError *error;
if (![[GANTracker sharedTracker] trackPageview:#"/in_app_purchase_error" withError:&error]) {
//NSLog(#"No GAN Tracking");
}
[self finishTransaction:transaction wasSuccessful:NO];
}
else
{
// this is fine, the user just cancelled, so don’t notify
NSError *error;
if (![[GANTracker sharedTracker] trackPageview:#"/in_app_purchase_cancel" withError:&error]) {
//NSLog(#"No GAN Tracking");
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
[self updateButtonStatus:#"ON"];
}
#pragma mark -
#pragma mark SKPaymentTransactionObserver methods
//
// called when the transaction status is updated
//
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
//[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
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;
}
}
}
#end
Chris, 2 things:-
Firstly, why have you opted for a multi-threaded approach?
Nothing here requires you spawn a new thread. The StoreKit api is asynchronous, as you know, you are afterall using the callbacks and delegates. This is specifically so it won't block the main thread and so you don't have to spawn a new thread. It almost certainly does it's work on a background thread - but you don't need to know that, it is handled for you. Infact, not only does this code not require a background thread, you are almost certainly experiencing a substantial performance cost spawning new threads to do so little work. ie. it will (probably) take longer for the thread to start up than it will to do the work you have scheduled on it.
So, if your motivation was performance, you are going to be disapointed.
Secondly, your threading code, or lack of it, is a mess. On the plus side, just to reiterate, none of it is needed, so no big problem.
You say that you are
using the same threading code in many
other places and it works fine
You have been unlucky. This has given you the impression that this should work when infact it is completely unsafe. Threading is really tough, and if you want to do it you could do worse than reading some the related apple docs
Threading
Concurrency
I'm reluctant to just spew stuff straight from these guides, translated thru my foggy brain, and try to pass it of as my own advice, but to try to motivate you to read the guides i've adding some comments to a couple of lines of your code:-
// you start a new background thread to call -loadStore
[NSThread detachNewThreadSelector:#selector(loadStore) toTarget:self withObject:nil];
// you initialize SKPaymentQueue singleton on the background thread - is this allowed? i dont know. I can't see it documented.
// then you add transactionObserver observer on the background thread - which thread do you want to receive notifications on? 1)Main thread, 2)this (background) thread, 3)unsure. If it's not the background thread this probably isnt a good idea
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
// Update the GUI from the background thread. No joking here - you absolutely can never do this. It's well documented.
[self doneSpinning];
// end of method, background thread exits or not? You tell me. hope we get lucky with those notifications
[pool release];
So, i'd like to add that i am definitely no expert on how In App Purchase works but i would bet there is nothing special about it. Your activity spinner will probably be fine if you get rid of the background thread or re-implement it in a thread safe manner (which in my opinion doesn't look worth the trouble here).
I only skimmed your code, but I saw at least two places where it looked like you are attempting to update UI (stopping a spinner, updating label text) from a method which is called from a thread that is not the primary (main) thread. This is not allowed - all UI must be updated from the main thread.
If you need to update UI from a background thread you need to marshall the call to the main thread, perhaps using peformSelectorOnMainThread:withObject:
So, for example, it looks like loadStore is called from a non-main thread, and it calls doneSpinning, which updates UI. I'd make the following change:
- (void)loadStore
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(#"Load Store");
// restarts any purchases if they were interrupted last time the app was open
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
// instead of calling doneSpinning directly, ensure it runs on the main thread
[self performSelectorOnMainThread: #selector( doneSpinning) withObject: nil];
[pool release];
}

Problem in In-App purchase-consumable model

I have created a non-consumable in-app purchase item and now I want to create a consumable in-app purchase item by which a user to buy it every time by using In-App purchase. Now what will have to change in following code in different model:-
in InApp purchase manager.m:
#implementation InAppPurchaseManager
//#synthesize purchasableObjects;
//#synthesize storeObserver;
#synthesize proUpgradeProduct;
#synthesize productsRequest;
//BOOL featureAPurchased;
//BOOL featureBPurchased;
//static InAppPurchaseManager* _sharedStoreManager; // self
- (void)dealloc {
//[_sharedStoreManager release];
//[storeObserver release];
[super dealloc];
}
- (void)requestProUpgradeProductData
{
NSSet *productIdentifiers = [NSSet setWithObject:#"com.comp_name.iWorkOut1" ];
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
// we will release the request object in the delegate callback
}
#pragma mark -
#pragma mark SKProductsRequestDelegate methods
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
//NSArray *products = response.products;
//proUpgradeProduct = [products count] == 1 ? [[products firstObject] retain]: nil;
if (proUpgradeProduct)
{
NSLog(#"Product title: %#", proUpgradeProduct.localizedTitle);
NSLog(#"Product description: %#", proUpgradeProduct.localizedDescription);
NSLog(#"Product price: %#", proUpgradeProduct.price);
NSLog(#"Product id:%#", proUpgradeProduct.productIdentifier);
}
/*for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
NSLog(#"Invalid product id: %#" , invalidProductId);
}*/
//finally release the reqest we alloc/init’ed in requestProUpgradeProductData
[productsRequest release];
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
}
#pragma -
#pragma Public methods
/* call this method once on startup*/
- (void)loadStore
{
/* restarts any purchases if they were interrupted last time the app was open*/
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
/* get the product description (defined in early sections)*/
[self requestProUpgradeProductData];
}
/* call this before making a purchase*/
- (BOOL)canMakePurchases
{
return [SKPaymentQueue canMakePayments];
}
/* kick off the upgrade transaction*/
- (void)purchaseProUpgrade
{
SKPayment *payment = [SKPayment paymentWithProductIdentifier:#"1212121"];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
#pragma -
#pragma Purchase helpers
/* saves a record of the transaction by storing the receipt to disk*/
- (void)recordTransaction:(SKPaymentTransaction *)transaction
{
if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseProUpgradeProductId])
{
/* save the transaction receipt to disk*/
[[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:#"proUpgradeTransactionReceipt" ];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
/* enable pro features*/
- (void)provideContent:(NSString *)productId
{
if ([productId isEqualToString:kInAppPurchaseProUpgradeProductId])
{
/* enable the pro features*/
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"isProUpgradePurchased" ];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful
{
// /* remove the transaction from the payment queue.*/
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, #"transaction" , nil];
if (wasSuccessful)
{
/* send out a notification that we’ve finished the transaction*/
[[NSNotificationCenter defaultCenter]postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo];
}
else
{
/* send out a notification for the failed transaction*/
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo];
}
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
[self recordTransaction:transaction];
[self provideContent:transaction.payment.productIdentifier];
[self finishTransaction:transaction wasSuccessful:YES];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
[self recordTransaction:transaction.originalTransaction];
[self provideContent:transaction.originalTransaction.payment.productIdentifier];
[self finishTransaction:transaction wasSuccessful:YES];
}
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
if (transaction.error.code != SKErrorPaymentCancelled)
{
/* error!*/
[self finishTransaction:transaction wasSuccessful:NO];
}
else
{
/* this is fine, the user just cancelled, so don’t notify*/
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
- (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;
}
}
}
#end
in SKProduct.m:-
#implementation SKProduct (LocalizedPrice)
- (NSString *)localizedPrice
{
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:self.priceLocale];
NSString *formattedString = [numberFormatter stringFromNumber:self.price];
[numberFormatter release];
return formattedString;
}
I suggest you look at InventoryKit, which provides support for non-consumable, consumable and subscription in app purchases, and is open source.