I have too many crash reports on system code. How can I find the issue on my application?
Crashed: com.apple.main-thread
EXC_BAD_ACCESS KERN_INVALID_ADDRESS at 0x81566238
libobjc.A.dylib objc_msgSend + 5 respondsToSelector:
StoreKit __34-[SKProductsRequest _handleReply:]_block_invoke + 446
libdispatch.dylib _dispatch_call_block_and_release + 10
UIKit UIApplicationMain + 1136
MyApp main.m line 13
UPDATED
The full code to process in-app purchases on the application:
#pragma mark In-App Purchase
- (NSString*)getProductId:(NSString*)feature {
NSBundle *bundle = [NSBundle mainBundle];
NSDictionary *info = [bundle infoDictionary];
NSString *bundleIdentifier = [info objectForKey: #"CFBundleIdentifier"];
return [NSString stringWithFormat:feature, [bundleIdentifier stringByReplacingOccurrencesOfString:#"-" withString:#""]];
}
- (void)requestItems:(NSString*)feature {
NSSet *productIdentifiers = [NSSet setWithObject:[self getProductId:feature]];
if ([feature isEqualToString:g100Items]) {
if (product100ItemsRequest) {
[product100ItemsRequest release];
product100ItemsRequest = nil;
}
product100ItemsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
product100ItemsRequest.delegate = self;
[product100ItemsRequest start];
// we will release the request object in the delegate callback
} else
if ([feature isEqualToString:gUnlimitedItems]) {
if (productUnlimitedItemsRequest) {
[productUnlimitedItemsRequest release];
productUnlimitedItemsRequest = nil;
}
productUnlimitedItemsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productUnlimitedItemsRequest.delegate = self;
[productUnlimitedItemsRequest start];
// we will release the request object in the delegate callback
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
[products addObjectsFromArray:response.products];
for (SKProduct *product in response.products) {
if (product && [product.productIdentifier isEqualToString:[self getProductId:g100Items]]) {
[button100Items setTitle:[NSString stringWithFormat:g100ItemsButton, product.localizedPrice] forState:UIControlStateNormal];
// finally release the reqest we alloc/init’ed in requestItems
[product100ItemsRequest release];
product100ItemsRequest = nil;
}
if (product && [product.productIdentifier isEqualToString:[self getProductId:gUnlimitedItems]]) {
[buttonUnlimitedItems setTitle:[NSString stringWithFormat:gUnlimitedItemsButton, product.localizedPrice] forState:UIControlStateNormal];
// finally release the reqest we alloc/init’ed in requestItems
[productUnlimitedItemsRequest release];
productUnlimitedItemsRequest = nil;
}
}
for (NSString *invalidProductId in response.invalidProductIdentifiers) {
NSLog(#"Invalid product id: %#" , invalidProductId);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
}
// call this method once on startup
- (void)loadStore:(BOOL)tryAgain {
if (tryAgain) {
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
NSMutableArray *oldProducts = products;
products = [[[NSMutableArray alloc] initWithObjects: nil] retain];
[oldProducts release];
}
// 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 requestItems:g100Items];
[self requestItems:gUnlimitedItems];
}
// call this before making a purchase
- (BOOL)canMakePurchases {
return [SKPaymentQueue canMakePayments];
}
// kick off the upgrade transaction
- (void)purchaseItems:(NSString*)feature {
bool ok = false;
for (SKProduct *product in products) {
if ([product.productIdentifier isEqualToString:[self getProductId:feature]]) {
SKPayment *payment = [SKPayment paymentWithProduct:product];
if (payment) {
[[SKPaymentQueue defaultQueue] addPayment:payment];
// Calling AppStore Dialog
}
ok = true;
break;
}
}
if (!ok) {
[self loadStore:YES];
[self showAlert:gInAppAlertTitle alertStr:gNoProductsToMakePurchase];
return;
}
}
// saves a record of the transaction by storing the receipt to disk
- (void)recordTransaction:(SKPaymentTransaction *)transaction {
if ([transaction.payment.productIdentifier isEqualToString:[self getProductId:g100Items]]) {
// save the transaction receipt to disk
[[NSUserDefaults standardUserDefaults] setValue:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]/*transaction.transactionReceipt*/ forKey:[self getProductId:g100Items]];
[[NSUserDefaults standardUserDefaults] synchronize];
} else
if ([transaction.payment.productIdentifier isEqualToString:[self getProductId:gUnlimitedItems]]) {
// save the transaction receipt to disk
[[NSUserDefaults standardUserDefaults] setValue:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]/*transaction.transactionReceipt*/ forKey:[self getProductId:gUnlimitedItems]];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
// enable pro features
- (bool)provideContent:(NSString *)productId {
if (productId) {
FirstViewController* firstViewController = [[tabBarController viewControllers] objectAtIndex:gSourceTabIndex];
if ([productId isEqualToString:[self getProductId:g100Items]]) {
firstViewController.itemCount += 100;
[firstViewController saveItems];
// 100 Items Provided
return true;
} else
if ([productId isEqualToString:[self getProductId:gUnlimitedItems]]) {
firstViewController.itemCount = gItemUnlimitedCount;
[firstViewController saveItems];
// Unlimited Items Provided
return true;
}
}
return false;
}
// 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];
}
}
// called when the transaction was successful
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
[self recordTransaction:transaction];
bool provided = [self provideContent:transaction.payment.productIdentifier];
[self finishTransaction:transaction wasSuccessful:YES];
}
// called when a transaction has been restored 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];
[self showAlert:gInAppAlertTitle alertStr:[transaction.error localizedDescription]];
} else {
// this is fine, the user just cancelled, so don’t notify
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
// 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;
}
}
}
- (IBAction)process100Items:(id)sender {
if ([self canMakePurchases]) {
[self purchaseItems:g100Items];
} else {
[self showAlert:gInAppAlertTitle alertStr:gCanNotMakePurchases];
}
// Buy 100 Items Button Pressed
}
- (IBAction)processUnlimitedItems:(id)sender {
if ([self canMakePurchases]) {
[self purchaseItems:gUnlimitedItems];
} else {
[self showAlert:gInAppAlertTitle alertStr:gCanNotMakePurchases];
}
// Buy Unlimited Items Button Pressed
}
- (IBAction)restoreCompletedTransactions:(id)sender {
if ([products count] == 0) {
[self loadStore:YES];
[self showAlert:gInAppAlertTitle alertStr:gNoProductsToMakePurchase];
return;
}
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
The code that is passed as a block to handleReply tries call a method on an object that is already released. Without further information than the provided stack trace, there is probably nothing more that can be said.
Related
I've successfully implemented one consumable product, however i have no clue to to implement multiple consumable products. Id like to addd more ProductIdentifiers like com.lalala.20batteries, com.lalala.30batteries
can anyone please give me some guides
here is my code for the single consumable product
#interface ViewController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
#end
#implementation ViewController
#define ProductIdentifier #"com.lalala.10batteries"
- (IBAction)taps10batteries{
NSLog(#"User requests to get 10 batteries");
if([SKPaymentQueue canMakePayments]){
NSLog(#"User can make payments");
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:ProductIdentifier]];
productsRequest.delegate = self;
[productsRequest start];
}
else{
NSLog(#"User cannot make payments 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");
}
}
- (IBAction)purchase:(SKProduct *)product{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(#"received restored transactions: %i", queue.transactions.count);
for(SKPaymentTransaction *transaction in queue.transactions){
if(transaction.transactionState == SKPaymentTransactionStateRestored){
//called when the user successfully restores a purchase
NSLog(#"Transaction state -> Restored");
[self get10Batteries];
[[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");
break;
case SKPaymentTransactionStatePurchased:
[self get10Batteries];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(#"Transaction state -> Purchased");
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Transaction state -> Restored");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
if(transaction.error.code == SKErrorPaymentCancelled){
NSLog(#"Transaction state -> Cancelled");
//the user cancelled the payment ;(
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
- (void)get10Batteries{
NSUbiquitousKeyValueStore *cloudstore1 = [NSUbiquitousKeyValueStore defaultStore];
//load cloud integer
coins = [cloudstore1 doubleForKey:#"yo" ];
coins = [[NSUserDefaults standardUserDefaults]
integerForKey:#"com.lalala.10batteries"];
coins += 10;
[[NSUserDefaults standardUserDefaults] setInteger:coins
forKey:#"com.lalala.10batteries"];
[[NSUserDefaults standardUserDefaults] synchronize];
_coinLabel.text = [NSString stringWithFormat:#"%li", (long)coins];
//save icloud
[cloudstore1 setDouble:coins forKey:#"yo"];
[cloudstore1 synchronize];
}
- (void)viewDidLoad {
[super viewDidLoad];
NSUbiquitousKeyValueStore *cloudstore1 = [NSUbiquitousKeyValueStore defaultStore];
_coinLabel.text = [cloudstore1 stringForKey:#"yo" ];
NSUserDefaults *coinsdefaults = [NSUserDefaults standardUserDefaults];
if([coinsdefaults objectForKey:#"com.lalala.10batteries"] != nil) {
coins = [[NSUserDefaults standardUserDefaults] integerForKey:#"com.lalala.10batteries"];
coins = [coinsdefaults integerForKey:#"com.lalala.10batteries"];
_coinLabel.text = [NSString stringWithFormat:#"%li", (long)coins];
}
}
Your code can easily support multiple products, just like it supports one...
First of all, declare your other product IDs just like you did with one:
#define ProductIdentifier1 #"com.lalala.10batteries"
#define ProductIdentifier2 #"com.lalala.20batteries"
Then, create a loadAllProducts method, which is exactly like the taps10batteries method, except you declare your SKProductsRequest with ALL of the product IDs, just like you did with one:
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObjects:ProductIdentifier1, ProductIdentifier2, nil]];
Unless you prefer loading each product individually, then you'll need to create a method for each product, i.e. taps20batteries and you can ignore the next part of my answer...
The callback method can return multiple products like it can return one. Handle the response as you see fit, just like you did with one:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
int count = [response.products count];
if(count > 0){
_products = [response.products sortedArrayUsingComparator:^(id a, id b) { //sort all products by price
NSDecimalNumber *first = [(SKProduct*)a price];
NSDecimalNumber *second = [(SKProduct*)b price];
return [second compare:first];
}];
for (SKProduct *product in _products) {
NSLog(#"Product Available: %#", product.productIdentifier);
//do something with the product..
//perhaps gather the products into an array and present it somehow to the user so s/he can select what to buy?
}
}
else {
NSLog(#"No products available");
}
}
If you want a full implementation that you can use, try this one
see the line where you create SKProductsRequest object:
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:ProductIdentifier]];
NSSet is a bag with objects inside unsorted and only one kind of allowed inside the bag (no dupes).
All you have to do is change it like this:
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObjects:
#"com.blahblah.unlockall",
#"com.blahblah.sounds",
#"com.blahblah.alarms",
#"com.blahblah.wallpapers",
nil]];
then u should have a custom wrapper class (CustomIAPHelper) as in the tutorial.
in view controller let's say you have a tableView then the code follows like this:
- (void)reload {
_products = nil;
[self.tableView reloadData];
[CustomIAPHelper sharedInstance].delegate = self;
[[CustomIAPHelper sharedInstance] requestProductsWithCompletionHandler:^(BOOL success, NSArray *products) {
if (success) {
// Sort the products array (by price)
_products = [products sortedArrayUsingComparator:^(id a, id b) {
NSDecimalNumber *first = [(SKProduct*)a price];
NSDecimalNumber *second = [(SKProduct*)b price];
return [second compare:first];
}];
[self.tableView reloadData];
}
[refreshControl endRefreshing];
[activity stopAnimating];
[activity setHidden:YES];
}];
}
remember to setup and implement iAPHelper's protocol methods.
I successfully implemented an in-app purchase to remove ads in my app. It worked the first time I tried it but after I ran the app on my phone a few more times it started to just show a white ad banner instead of hidding the ad banner like it used to.
Here is the code for the StartScreen.m of my app as well as the PurchaseViewController.m to buy the IAP to remove ads. I also got a Warning saying I am using 10 instances of ADBanner even though I have them removed whenever the view disappers. Thank you for any and all help.
//
//StartScreen.m
//
#interface StartScreen ()
{
BOOL _bannerIsVisible;
}
#end
#implementation StartScreen
- (void)viewDidLoad {
//Ads
self.adBanner.delegate = self;
}
- (void)viewWillDisappear:(BOOL)animated
{
[self.adBanner removeFromSuperview];
self.adBanner.delegate = nil;
self.adBanner = nil;
}
- (void)bannerViewDidLoadAd:(ADBannerView *)banner
{
// Check for Remove Ads IAP
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
if ([prefs boolForKey:#"RemoveAds"] == TRUE) {
self.adBanner.hidden = YES;
_bannerIsVisible = NO;
} else if (!_bannerIsVisible)
{
self.adBanner.hidden = NO;
_bannerIsVisible = YES;
}
}
- (void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
NSLog(#"Failed to retreive ad");
// Check for Remove Ads IAP
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
if ([prefs boolForKey:#"RemoveAds"] == TRUE) {
self.adBanner.hidden = YES;
_bannerIsVisible = NO;
}
}
//
// PurcahseViewController.m
//
#import "PurcahseViewController.h"
#implementation PurcahseViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.productID = #"com.app.iap1";
[self getProductID:self];
self.buyButton.enabled = NO;
NSLog(#"%#", self.productID);
}
- (void)getProductID:(UIViewController *)viewController {
if ([SKPaymentQueue canMakePayments]) {
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:self.productID]];
request.delegate = self;
[request start];
} else {
self.productDescription.text = #"Please enable in app purchase in your settings";
}
}
- (IBAction)purchaseItem:(id)sender {
SKPayment *payment = [SKPayment paymentWithProduct:self.product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (IBAction)restoreButton:(id)sender {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
[self UnlockPurchase];
}
- (void)Purchased {
NSLog(#"Item has been purchased");
}
#pragma mark _
#pragma mark SKProductsRequestDelegate
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSArray *products = response.products;
if (products.count != 0) {
self.product = products[0];
self.buyButton.enabled = YES;
self.productTitle.text = self.product.localizedTitle;
self.productDescription.text = self.product.localizedDescription;
} else {
self.productTitle.text = #"404: Product Not Found";
}
products = response.invalidProductIdentifiers;
for (SKProduct *product in products) {
NSLog(#"404: Product Not Found: %#", product);
}
}
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased: [self UnlockPurchase];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:NSLog(#"Transaction Failed");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
default:
break;
}
}
}
-(void)UnlockPurchase {
self.buyButton.enabled = NO;
[self.buyButton setTitle:#"Purchased" forState:UIControlStateDisabled];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setBool:TRUE forKey:#"RemoveAds"];
[prefs synchronize];
[self Purchased];
}
First off, to answer your question, the reason your banner isn't hiding when your ad fails is because you're not hiding it. Whether or not [prefs boolForKey:#"RemoveAds"] == TRUE, you're going to want to hide that banner if you don't want the blank white bar in its place. To do so in the simplest possible way (without any animation), simplify your didFailToReceiveAdWithError: method, like so:
- (void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
NSLog(#"Failed to retreive ad");
// Check for Remove Ads IAP
self.adBanner.hidden = YES;
_bannerIsVisible = NO;
}
And hide it in your viewDidLoad so it's hidden from the very beginning before any ad has loaded:
- (void)viewDidLoad {
//Ads
self.adBanner.delegate = self;
self.adBanner.hidden = YES;
}
That way, your ad will only unhide in bannerViewDidLoadAd: once an ad has successfully been loaded.
Secondly, your _bannerIsVisible boolean is unnecessary. Instead of using a separate boolean to check whether the banner is visible, you can just check whether it's hidden, with if (self.adBanner.hidden == YES) or if (self.adBanner.hidden == NO) or if (self.adBanner.hidden) or if (!self.adBanner.hidden). Eliminating that unnecessary boolean will cut down on the potential for error.
So, just to summarize, here's my suggestion for how your bannerViewDidLoadAd: and didFailToReceiveAdWithError: methods should look:
- (void)bannerViewDidLoadAd:(ADBannerView *)banner {
// Check for Remove Ads IAP
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
if ([prefs boolForKey:#"RemoveAds"] == TRUE) {
self.adBanner.hidden = YES;
} else if (self.adBanner.hidden) {
self.adBanner.hidden = NO;
}
}
- (void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
NSLog(#"Failed to retreive ad");
self.adBanner.hidden = YES;
}
I have an app with in app purchases.
These in app purchases have apple hosted content associated with them.
So a user tap, buys and downloads the content.
So far so good, everything works properly.
In order to have the downloads work properly I can't call before the download ends:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction]]
My problem arises when the user locks and unlocks the device.
After the device is unlocked the paymentQueue gets called again with whatever is downloading which I ignore in code but the dialog box requesting the user's password arises again.
This is completely redundant since the download is still running in the background.
If I hit cancel nothing changes and the download proceeds to finish.
If I complete my password a new transaction is added to the queue, so the same file is now downloading twice.
If I close down the app and start up again, again the dialog for my password is shown, which if I ignore starts downloading properly. If I enter my password another product is queued.
I have made sure that there are no duplicate observers and have narrowed down the problem to not calling finishTransactionwhich I can't, since that cancels the download.
My code below:
+ (id)sharedSingleton {
if ( !shared ) {
shared = [[InAppPurchaseManager alloc] initWithProductIdentifiers:nil];
}
return shared;
}
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers {
if ( !shared ) {
self = [super init];
shared = self;
} else {
self = shared;
}
if ( self && productIdentifiers ) {
_paused = false;
_productIdentifiers = productIdentifiers;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)requestProducts {
//if there is a new request cancel the old one
if ( _productsRequest ) {
[_productsRequest setDelegate:nil];
[_productsRequest cancel];
_productsRequest= nil;
}
_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
_productsRequest.delegate = self;
[_productsRequest start];
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
_productsRequest = nil;
_products = [NSMutableDictionary dictionary];
for (SKProduct * skProduct in response.products) {
[_products setObject:skProduct forKey:skProduct.productIdentifier];
}
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
_productsRequest = nil;
if ( error ) {
[self handleError:error];
}
}
- (BOOL)checkIfProductPurchased:(NSString *)productIdentifier {
return [self getStateFromUserDefaultsForInAppID:productIdentifier] != NOT_BOUGHT;
}
#pragma mark - Purchasing
- (void)buyProductWithInAppID:(NSString *)inAppId {
SKProduct *product = [_products objectForKey:inAppId];
if ( product ) {
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
} else {
NSMutableDictionary *details = [NSMutableDictionary dictionary];
[details setValue:#"Can't reach Apple's servers. Please try again. If not connected to the internet, please connect and try again." forKey:NSLocalizedDescriptionKey];
NSError *error = [NSError errorWithDomain:#"In App Error" code:200 userInfo:details];
[self handleError:error];
[self requestProducts];
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
break;
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
};
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
if (transaction.downloads) {
NSString *iap = transaction.payment.productIdentifier;
int state = [self getStateFromUserDefaultsForInAppID:iap];
switch (state) {
case NOT_BOUGHT:
case BOUGHT:
NSLog(#"start download after purchase: %#", iap);
[self provideContentForProductIdentifier:iap restored:NO];
[self setStateForUserDefaults:DOWNLOADING ForInAppID:iap];
[[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];
break;
case DOWNLOADING:
NSLog(#"downloading");
//ignore
break;
case DOWNLOADED:
NSLog(#"downloaded");
//finish transaction
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self transactionFinished];
break;
default:
break;
}
} else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self transactionFinished];
}
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
BOOL shouldRestore = false;
for ( NSString *iap in _restoreInAppArray ) {
if ( [transaction.payment.productIdentifier isEqualToString:iap] ) {
shouldRestore = true;
break;
}
}
if ( (_restoreInAppArray != nil && shouldRestore) || _restoreInAppArray == nil ) {
NSString *iap = transaction.payment.productIdentifier;
int state = [self getStateFromUserDefaultsForInAppID:iap];
switch (state) {
case NOT_BOUGHT:
case BOUGHT:
NSLog(#"start download after restore: %#", iap);
[self provideContentForProductIdentifier:iap restored:YES];
[self setStateForUserDefaults:DOWNLOADING ForInAppID:iap];
[[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];
break;
case DOWNLOADING:
//ignore
break;
case DOWNLOADED:
//finish transaction
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self transactionFinished];
break;
default:
break;
}
} else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self transactionFinished];
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads {
_activeDownloads = [downloads copy];
for (SKDownload *download in downloads) {
if (download.downloadState == SKDownloadStateFinished) {
[self processDownload:download];
[self setStateForUserDefaults:DOWNLOADED ForInAppID:download.contentIdentifier];
[queue finishTransaction:download.transaction];
[self transactionFinished];
} else if (download.downloadState == SKDownloadStateActive) {
NSString *productID = download.contentIdentifier;
NSTimeInterval remaining = download.timeRemaining;
float progress = download.progress;
//int mbs = round(download.contentLength/1024/1024);
if ( delegate && [delegate respondsToSelector:#selector(downloadProgress:forInAppID:timeRemaining:totalMB:)] ) {
[delegate downloadProgress:progress forInAppID:productID timeRemaining:remaining totalMB:0];
}
} else if ( download.downloadState == SKDownloadStateFailed ) {
if ( !_paused && [self getStateFromUserDefaultsForInAppID:download.contentIdentifier] != DOWNLOADED) {
NSLog(#"Download Failed %li: %#", (long)download.downloadState, download.contentIdentifier);
[queue startDownloads:[NSArray arrayWithObject:download]];
}
} else if ( download.downloadState == SKDownloadStateWaiting || download.downloadState == SKDownloadStatePaused ) {
if ( !_paused && [self getStateFromUserDefaultsForInAppID:download.contentIdentifier] != DOWNLOADED) {
NSLog(#"Download Wait or Paused %li: %#", (long)download.downloadState, download.contentIdentifier);
[queue resumeDownloads:[NSArray arrayWithObject:download]];
}
} else {
NSLog(#"Cancelled");
[self setStateForUserDefaults:BOUGHT ForInAppID:download.contentIdentifier];
[queue finishTransaction:download.transaction];
}
}
}
- (void)pauseAllDownloads {
NSLog(#"pauseAllDownloads");
_paused = true;
if ( _activeDownloads ) {
[[SKPaymentQueue defaultQueue] pauseDownloads:_activeDownloads];
}
}
- (void)startAllDownloads {
NSLog(#"startAllDownloads");
_paused = false;
if ( _activeDownloads ) {
for ( SKDownload *download in _activeDownloads ) {
//NSLog(#"downloads: %# state: %i", download.contentIdentifier, download.downloadState);
if ( download.downloadState == SKDownloadStatePaused || download.downloadState == SKDownloadStateFailed ) {
[[SKPaymentQueue defaultQueue] resumeDownloads:[NSArray arrayWithObject:download]];
} else if ( download.downloadState == SKDownloadStateWaiting ) {
[[SKPaymentQueue defaultQueue] startDownloads:[NSArray arrayWithObject:download]];
}
}
}
}
- (void)processDownload:(SKDownload*)download {
NSLog(#"processDownloads");
// convert url to string, suitable for NSFileManager
NSString *path = [download.contentURL path];
// files are in Contents directory
path = [path stringByAppendingPathComponent:#"Contents"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fileManager contentsOfDirectoryAtPath:path error:&error];
NSString *dir = [FileLocation getFolderToSaveWithDocumentDirectory:TRUE];
for (NSString *file in files) {
NSString *fullPathSrc = [path stringByAppendingPathComponent:file];
NSString *fullPathDst = [dir stringByAppendingPathComponent:file];
// not allowed to overwrite files - remove destination file
[fileManager removeItemAtPath:fullPathDst error:NULL];
if ([fileManager moveItemAtPath:fullPathSrc toPath:fullPathDst error:&error] == NO) {
NSLog(#"Error: unable to move item: %#", error);
}
[FileLocation setICloudFlagForFile:fullPathDst];
}
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductDownloadComplete object:download.contentIdentifier];
}
- (void)cancelProductDownloadWithAppID:(NSString *)inAppId {
for ( SKDownload *download in _activeDownloads ) {
if ( [download.contentIdentifier isEqualToString:inAppId] ) {
NSLog(#"Canceling: %#", inAppId);
[[SKPaymentQueue defaultQueue] cancelDownloads:[NSArray arrayWithObject:download]];
[[SKPaymentQueue defaultQueue] finishTransaction:download.transaction];
[self transactionFinished];
}
}
}
- (void)restoreProductWithInAppIDArray:(NSArray *)inAppId {
NSLog(#"restoreProductWithInAppIDArray: %#", inAppId);
_restoreInAppArray = inAppId;
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
NSLog(#"failedTransaction");
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchaseFailedNotification object:nil userInfo:nil];
if ( transaction.error.code != SKErrorPaymentCancelled ) {
[self handleError:transaction.error];
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
[self transactionFinished];
}
- (void)provideContentForProductIdentifier:(NSString *)productIdentifier restored:(BOOL)restored {
if ( restored ) {
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductRestoredNotification object:productIdentifier userInfo:nil];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification object:productIdentifier userInfo:nil];
}
}
- (void)restoreCompletedTransactions {
NSLog(#"restoreCompletedTransactions");
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)transactionFinished {
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductTransactionEnded object:nil userInfo:nil];
}
- (void)setStateForUserDefaults:(int)state ForInAppID:(NSString *)iap {
NSLog(#"-setting state: %i %#", state, iap);
[[NSUserDefaults standardUserDefaults] setInteger:state forKey:iap];
[[NSUserDefaults standardUserDefaults] synchronize];
}
+ (void)setStateForUserDefaults:(int)state ForInAppID:(NSString *)iap {
NSLog(#"+setting state: %i %#", state, iap);
[[NSUserDefaults standardUserDefaults] setInteger:state forKey:iap];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (int)getStateFromUserDefaultsForInAppID:(NSString *)iap {
return (int)[[NSUserDefaults standardUserDefaults] integerForKey:iap];
}
+ (int)getStateFromUserDefaultsForInAppID:(NSString *)iap {
return (int)[[NSUserDefaults standardUserDefaults] integerForKey:iap];
}
- (void)dealloc {
NSLog(#"dealloc in app manager");
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_productsRequest cancel];
_productsRequest.delegate = nil;
}
I'm trying to implement ray wenderlich example: http://www.raywenderlich.com/36270/in-app-purchases-non-renewing-subscription-tutorial
Purchasing product return me no error at all at the first time.
When I'm trying to restore the purchase I have a response with 0 transactions to restore in method:
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
Strangely, when I'm trying to buy again same product, I've an alert that I already purchased the item and I can renew or extend.
Why can't I restore the transaction while trying to purchase it again show me otherwise?
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers
{
if ((self = [super init])) {
// Store product identifiers
_productIdentifiers = productIdentifiers;
// Check for previously purchased products
_purchasedProductIdentifiers = [NSMutableSet set];
for (NSString * productIdentifier in _productIdentifiers) {
BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
if (productPurchased) {
[_purchasedProductIdentifiers addObject:productIdentifier];
NSLog(#"Previously purchased: %#", productIdentifier);
} else {
NSLog(#"Not purchased: %#", productIdentifier);
}
}
// Add self as transaction observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler
{
_completionHandler = [completionHandler copy];
_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
_productsRequest.delegate = self;
[_productsRequest start];
}
- (BOOL)productPurchased:(NSString *)productIdentifier
{
NSLog(#"%s, productIdentifier: %#", __PRETTY_FUNCTION__, productIdentifier);
return [_purchasedProductIdentifiers containsObject:productIdentifier];
}
- (void)buyProduct:(SKProduct *)product
{
NSLog(#"Buying %#...", product.productIdentifier);
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void)validateReceiptForTransaction:(SKPaymentTransaction *)transaction
{
VerificationController * verifier = [VerificationController sharedInstance];
[verifier verifyPurchase:transaction completionHandler:^(BOOL success) {
if (success) {
NSLog(#"Successfully verified receipt!");
[self provideContentForProductIdentifier:transaction.payment.productIdentifier];
}
else {
NSLog(#"Failed to validate receipt.");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}];
}
-(int)daysRemainingOnSubscription
{
NSDate * expiryDate = [[NSUserDefaults standardUserDefaults] objectForKey:kSubscriptionExpirationDateKey];
NSDateFormatter *dateformatter = [NSDateFormatter new];
[dateformatter setDateFormat:#"dd MM yyyy"];
NSTimeInterval timeInt = [[dateformatter dateFromString:[dateformatter stringFromDate:expiryDate]] timeIntervalSinceDate: [dateformatter dateFromString:[dateformatter stringFromDate:[NSDate date]]]]; //Is this too complex and messy?
int days = timeInt / 60 / 60 / 24;
if (days >= 0) {
return days;
} else {
return 0;
}
}
-(NSString *)getExpiryDateString
{
if ([self daysRemainingOnSubscription] > 0) {
NSDate *today = [[NSUserDefaults standardUserDefaults] objectForKey:kSubscriptionExpirationDateKey];
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd/MM/yyyy"];
return [NSString stringWithFormat:#"Subscribed! \nExpires: %# (%i Days)",[dateFormat stringFromDate:today],[self daysRemainingOnSubscription]];
} else {
return #"Not Subscribed";
}
}
-(NSDate *)getExpiryDateForMonths:(int)months
{
NSDate *originDate;
if ([self daysRemainingOnSubscription] > 0) {
originDate = [[NSUserDefaults standardUserDefaults] objectForKey:kSubscriptionExpirationDateKey];
} else {
originDate = [NSDate date];
}
NSDateComponents *dateComp = [[NSDateComponents alloc] init];
[dateComp setMonth:months];
return [[NSCalendar currentCalendar] dateByAddingComponents:dateComp toDate:originDate options:0];
}
-(void)purchaseSubscriptionWithMonths:(int)months
{
NSDate *expiryDate = [self getExpiryDateForMonths:months];
[[NSUserDefaults standardUserDefaults] setObject:expiryDate forKey:kSubscriptionExpirationDateKey];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(#"Subscription Complete!");
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSLog(#"Loaded list of products...");
_productsRequest = nil;
NSArray * skProducts = response.products;
for (SKProduct * skProduct in skProducts) {
NSLog(#"Found product: %# %# %0.2f",
skProduct.productIdentifier,
skProduct.localizedTitle,
skProduct.price.floatValue);
}
_completionHandler(YES, skProducts);
_completionHandler = nil;
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
NSLog(#"Failed to load list of products.");
_productsRequest = nil;
_completionHandler(NO, nil);
_completionHandler = nil;
}
#pragma mark SKPaymentTransactionOBserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
NSLog(#"%s", __PRETTY_FUNCTION__);
for (SKPaymentTransaction * transaction in transactions) {
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
NSLog(#"transaction.transactionState == SKPaymentTransactionStatePurchased");
[self completeTransaction:transaction];
}
else if (transaction.transactionState == SKPaymentTransactionStateRestored) {
NSLog(#"transaction.transactionState == SKPaymentTransactionStateRestored");
[self restoreTransaction:transaction];
}
else if (transaction.transactionState == SKPaymentTransactionStateFailed) {
NSLog(#"transaction.transactionState == SKPaymentTransactionStateFailed");
[self failedTransaction:transaction];
}
else {
NSLog(#"None of the above.");
}
};
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"completeTransaction...");
[self validateReceiptForTransaction:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"restoreTransaction...");
[self validateReceiptForTransaction:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"failedTransaction...");
if (transaction.error.code != SKErrorPaymentCancelled)
{
NSLog(#"Transaction error: %#", transaction.error.localizedDescription);
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void)provideContentForProductIdentifier:(NSString *)productIdentifier
{
NSLog(#"%s, productIdentifier: %#", __PRETTY_FUNCTION__, productIdentifier);
if ([productIdentifier isEqualToString:#"com.cellularradar.onemonth"]) {
[self purchaseSubscriptionWithMonths:1];
}
else if ([productIdentifier isEqualToString:#"com.cellularradar.threemonths"]) {
[self purchaseSubscriptionWithMonths:3];
}
else if ([productIdentifier isEqualToString:#"com.cellularradar.sixmonths"]) {
[self purchaseSubscriptionWithMonths:6];
}
else if ([productIdentifier isEqualToString:#"com.cellularradar.twelvemonths"]) {
[self purchaseSubscriptionWithMonths:12];
}
[_purchasedProductIdentifiers addObject:productIdentifier];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
[[NSUserDefaults standardUserDefaults] synchronize];
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification
object:productIdentifier
userInfo:nil];
}
- (void)restoreCompletedTransactions
{
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSMutableArray *purchasedItemsIDs = [[NSMutableArray alloc] init];
NSLog(#"Received restored transaction: %lu", (unsigned long)queue.transactions.count);
purchasedItemsIDs = [queue.transactions valueForKeyPath:#"payment.productIdentifier"];
NSLog(#"purchasedItemsIDs: %#", purchasedItemsIDs);
for (SKPaymentTransaction *transaction in queue.transactions) {
NSString *productID = transaction.payment.productIdentifier;
[purchasedItemsIDs addObject:productID];
NSLog(#"productID(transaction.payment.productIdentifier): %#", productID);
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
NSLog(#"%s, %#", __PRETTY_FUNCTION__, error.localizedDescription);
}
Judging by the message that you received "Tap buy to renew or extend it", it seems that your product is a non-renewing subscription. This product type is not restorable by StoreKit API and you must do it yourself (most likely via checking on your server).
A quote from In-App Purchase Programming Guide (Restoring Purchased Products):
If your app uses non-renewing subscriptions, your app is responsible for the restoration process.
Here is a comparison of subscription types from In-App Purchase Programming Guide:
+------------------------+----------------+----------------+---------------+
| Subscription type | Auto-renewable | Non-renewing | Free |
+------------------------+----------------+----------------+---------------+
| Users can buy | Multiple times | Multiple times | Once |
| Appears in the receipt | Always | Once | Always |
| Synced across devices | By the system | By your app | By the system |
| Restored | By the system | By your app | By the system |
+------------------------+----------------+----------------+---------------+
In the tutorial you linked to, the last section shows you how you should implement restoring.
Here is the code from that section:
- (void)restoreTapped:(id)sender {
[[RageIAPHelper sharedInstance] restoreCompletedTransactions];
//1
if ([PFUser currentUser].isAuthenticated) {
PFQuery *query = [PFQuery queryWithClassName:#"_User"];
[query getObjectInBackgroundWithId:[PFUser currentUser].objectId block:^(PFObject *object, NSError *error) {
//2
NSDate *serverDate = [[object objectForKey:kSubscriptionExpirationDateKey] lastObject];
[[NSUserDefaults standardUserDefaults] setObject:serverDate forKey:kSubscriptionExpirationDateKey];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.tableView reloadData];
NSLog(#"Restore Complete!");
}];
}
Another method would be to restore the purchase by checking the receipt data and calculating the subscription accordingly.
As already mentioned it is not possible to restore the transactions as such as they are consumables, however, you can get the receipt and parse this in order to manage restoration / calculations.
I am confused about how to and when to tell the user that they completed the purchase successfully. I got my application rejected during the app review process for this reason:
1. Launch app
2. Tap on learn about the benefits of subscription
3. Tap on Subscribe
4. Tap on Confirm and enter iTunes password
5. No further action occurs
And I am not sure when and how to tell the user they entered their info correctly since that is confirmed on the iTunes server.
I have an IAPHelper class which looks like this:
//
// IAPHelper.m
// BusinessPlan
//
// Created by MacOSLion on 8/12/13.
//
//
// 1
#import "IAPHelper.h"
#import <StoreKit/StoreKit.h>
// 2
//#interface IAPHelper () <SKProductsRequestDelegate>
#interface IAPHelper () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
#end
#implementation IAPHelper
{
// 3
SKProductsRequest * _productsRequest;
// 4
RequestProductsCompletionHandler _completionHandler;
NSSet * _productIdentifiers;
NSMutableSet * _purchasedProductIdentifiers;
}
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers
{
if ((self = [super init]))
{
// Store product identifiers
_productIdentifiers = productIdentifiers;
// Check for previously purchased products
_purchasedProductIdentifiers = [NSMutableSet set];
for (NSString * productIdentifier in _productIdentifiers)
{
BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
if (productPurchased)
{
[_purchasedProductIdentifiers addObject:productIdentifier];
NSLog(#"Previously purchased: %#", productIdentifier);
// SET memory to yes and then use that later.
// Get user data.
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
// First time on the app, so set the user cookie.
[standardUserDefaults setBool:YES forKey:#"subscriber"];
// Saving
[[NSUserDefaults standardUserDefaults] synchronize];
}
else
{
NSLog(#"Not purchased: %#", productIdentifier);
}
}
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
// retrieve the product information from iTunes Connect
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler
{
// 1
_completionHandler = [completionHandler copy];
// 2
_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
_productsRequest.delegate = self;
[_productsRequest start];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSLog(#"Loaded list of products...");
_productsRequest = nil;
NSArray * skProducts = response.products;
for (SKProduct * skProduct in skProducts)
{
NSLog(#"Found product: %# %# %0.2f",
skProduct.productIdentifier,
skProduct.localizedTitle,
skProduct.price.floatValue);
}
_completionHandler(YES, skProducts);
_completionHandler = nil;
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
NSLog(#"Failed to load list of products.");
_productsRequest = nil;
_completionHandler(NO, nil);
_completionHandler = nil;
}
- (BOOL)productPurchased:(NSString *)productIdentifier
{
return [_purchasedProductIdentifiers containsObject:productIdentifier];
}
- (void)buyProduct:(SKProduct *)product
{
NSLog(#"Buying %#...", product.productIdentifier);
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (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)completeTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"completeTransaction...");
[self provideContentForProductIdentifier:transaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
// SET memory to yes and then use that later.
// Get user data.
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
// First time on the app, so set the user cookie.
[standardUserDefaults setBool:YES forKey:#"subscriber"];
// Saving
[[NSUserDefaults standardUserDefaults] synchronize];
// Tell user that things are purchased.
// MESSAGE PERSON THAT CAN'T CONNECT TO SERVER
// UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Success sending purchase request."
// message:#"Just press OK and wait a few moments while iTunes processes the request." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
//
// [message show];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"restoreTransaction...");
[self provideContentForProductIdentifier:transaction.originalTransaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"failedTransaction...");
if (transaction.error.code != SKErrorPaymentCancelled)
{
NSLog(#"Transaction error: %#", transaction.error.localizedDescription);
// MESSAGE PERSON THAT CAN'T CONNECT TO SERVER
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Could not complete your transaction"
message:#"Please try again. If the error persists, please email support at: alex#problemio.com" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[message show];
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
// Add to top of file
NSString *const IAPHelperProductPurchasedNotification = #"IAPHelperProductPurchasedNotification";
// Add new method
- (void)provideContentForProductIdentifier:(NSString *)productIdentifier
{
//NSLog(#"Provifing content for subsciber: ");
// MESSAGE PERSON THAT CAN'T CONNECT TO SERVER
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Subscribed successfully!"
message:#"Now you can ask questions right on the app, and get our monthly business content." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[message show];
[_purchasedProductIdentifiers addObject:productIdentifier];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
[[NSUserDefaults standardUserDefaults] synchronize];
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification object:productIdentifier userInfo:nil];
}
#end
And my class from which I start the transaction process:
#import "SubscriptionController.h"
// 1
#import "RageIAPHelper.h"
#import <StoreKit/StoreKit.h>
// 2
#interface SubscriptionController ()
{
NSArray *_products;
// Add new instance variable to class extension
NSNumberFormatter * _priceFormatter;
}
#end
#implementation SubscriptionController
// 3
- (void)viewDidLoad
{
[super viewDidLoad];
//self.refreshControl = [[UIRefreshControl alloc] init];
//[self.refreshControl addTarget:self action:#selector(reload) forControlEvents:UIControlEventValueChanged];
[self reload];
//[self.refreshControl beginRefreshing];
// Add to end of viewDidLoad
_priceFormatter = [[NSNumberFormatter alloc] init];
[_priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[_priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
self.view.backgroundColor = [UIColor colorWithWhite:0.859 alpha:1.000];
}
// 4
- (void)reload
{
_products = nil;
//[self.tableView reloadData];
[[RageIAPHelper sharedInstance] requestProductsWithCompletionHandler:^(BOOL success, NSArray *products)
{
if (success)
{
_products = products;
//[self.tableView reloadData];
}
//[self.refreshControl endRefreshing];
}];
}
#pragma mark - Table View
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
// 5
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _products.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"a");
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
SKProduct * product = (SKProduct *) _products[indexPath.row];
cell.textLabel.text = product.localizedTitle;
// Add to bottom of tableView:cellForRowAtIndexPath (before return cell)
[_priceFormatter setLocale:product.priceLocale];
cell.detailTextLabel.text = [_priceFormatter stringFromNumber:product.price];
if ([[RageIAPHelper sharedInstance] productPurchased:product.productIdentifier])
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
cell.accessoryView = nil;
}
else
{
UIButton *buyButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
buyButton.frame = CGRectMake(0, 0, 72, 37);
[buyButton setTitle:#"Buy" forState:UIControlStateNormal];
buyButton.tag = indexPath.row;
[buyButton addTarget:self action:#selector(buyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
cell.accessoryType = UITableViewCellAccessoryNone;
cell.accessoryView = buyButton;
}
return cell;
}
//- (IBAction)subscribe:(id)sender
//{
// UIButton *buyButton = (UIButton *)sender;
// SKProduct *product = _products[buyButton.tag];
//
// NSLog(#"Buying %#...", product.productIdentifier);
// [[RageIAPHelper sharedInstance] buyProduct:product];
//}
- (void)viewWillAppear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(productPurchased:) name:IAPHelperProductPurchasedNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)productPurchased:(NSNotification *)notification
{
NSLog(#"PURCHASEDDDDDDDDD");
// NSString * productIdentifier = notification.object;
// [_products enumerateObjectsUsingBlock:^(SKProduct * product, NSUInteger idx, BOOL *stop)
// {
// if ([product.productIdentifier isEqualToString:productIdentifier])
// {
// // TODO:
// // Update how the button appears.
//
//
//// [self.table reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:idx inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
// *stop = YES;
// }
// }];
// MESSAGE PERSON THAT CAN'T CONNECT TO SERVER
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Purchased successfully"
message:#":)" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[message show];
// PUSH TO CONFIRMATION
}
//- (IBAction)subscribe:(id)sender
//{
//
//}
- (void)viewDidUnload
{
[super viewDidUnload];
}
- (IBAction)createSub:(id)sender
{
UIButton *buyButton = (UIButton *)sender;
SKProduct *product = _products[buyButton.tag];
if ( product == nil)
{
// MESSAGE PERSON THAT CAN'T CONNECT TO SERVER
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Pulling product data from iTunes..."
message:#"Please try again in a few moments." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[message show];
}
else
{
// MESSAGE PERSON THAT CAN'T CONNECT TO SERVER
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Success sending purchase request."
message:#"Just press OK and wait a few moments while iTunes processes the request." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[message show];
NSLog(#"Buying %#...", product.productIdentifier);
[[RageIAPHelper sharedInstance] buyProduct:product];
}
}
#end
Thank you for your help!
You should have some sort of UI update to tell the user that the payment was successful and the feature is now available/unlocked. Typically, this is done either with an update in your views to correspond to the new content, or a UIAlertView if there are no visual changes made.