Please be patient. This is my first time setting up an in-app purchase and I will try and provide as much information as I can. Instead of grooming the internet for someone else's code I ended up purchasing a class from infinite skills specifically on how to add a non-renewing subscription in-app purchase to my app. Their class was outdated, and come to find out, the "follow along" project files did not download. So I did a lot of research and this is what I came up with:
I created an in-app purchase in itunes, the in-app identifier matches the identifier in my .m coding.
I created a new provisioning profile and enabled in-app purchases and the icloud to manage the backend for the purchase. When I returned to the app i enabled icloud and made sure in-app purchases was turned on in the project target.
I created a view controller, added buttons and for the subclass I used SKStoreProductViewController. That View Controller looks like this:
I imported the storekit and the SK delegates.
It crashes when I hit the tabbarbutton from my home view to bring me to the in-app view controller.
Finally the coding:
InAppViewController.h:
#import <StoreKit/StoreKit.h>
#interface InAppViewController : SKStoreProductViewController
#end
InAppViewController.m:
//
// InAppViewController.m
// Contractor Rich
//
// Created by Joshua Hart on 2/1/15.
// Copyright (c) 2015 Code By Hart. All rights reserved.
//
#import "InAppViewController.h"
#import <StoreKit/StoreKit.h>
#interface InAppViewController ()<SKProductsRequestDelegate, SKPaymentTransactionObserver>
#property (weak, nonatomic) IBOutlet UIButton *btnBuyAccess;
#property (weak, nonatomic) IBOutlet UIButton *btnPremiumFeature;
#property (weak, nonatomic) IBOutlet UILabel *lblStatus;
#property (strong, nonatomic) SKProduct *product;
#property (strong, nonatomic) NSUbiquitousKeyValueStore *keyStore;
#property (strong, nonatomic) NSDate *expirationDate;
- (IBAction)btnBuyAccessTouched: (id)sender;
-(void)getProductInfo;
#end
#implementation InAppViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_btnBuyAccess.userInteractionEnabled = NO;
_keyStore = [[NSUbiquitousKeyValueStore alloc] init];
_expirationDate = [_keyStore objectForKey:#"expirationDate"];
NSDate *today = [NSDate date];
if (_expirationDate == nil)
_expirationDate = today;
if (_expirationDate > today) {
_btnPremiumFeature.userInteractionEnabled = YES;
}
else {
_btnBuyAccess.userInteractionEnabled = YES;
[self getProductInfo];
}
}
-(void) getProductInfo{
if ([SKPaymentQueue canMakePayments])
{
NSMutableArray *productIdentifierList = [[NSMutableArray alloc] init];
[productIdentifierList addObject:[NSString stringWithFormat:#"com.joshua.contractorrich.inapp"]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithArray:productIdentifierList]];
request.delegate = self;
[request start];
}
}
-(void) productsRequest: (SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *products = response.products;
if (products.count != 0)
{
_product = products[0];
_btnBuyAccess.userInteractionEnabled = YES;
_lblStatus.text = #"Ready for Purchase!";
}else{
_lblStatus.text = #"Product was not Found!";
}
products = response.invalidProductIdentifiers;
for (SKProduct *product in products)
{
NSLog(#"Product not found: %#", product);
}
}
- (IBAction)btnBuyAccessTouched: (id)sender {
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:
_btnPremiumFeature.userInteractionEnabled = YES;
_lblStatus.text = #"Purchase Completed!";
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed: NSLog(#"Transaction Failed!");
_lblStatus.text = #"Purchase Failed!";
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
default:
break;
}
}
}
-(void) setExpirationDate {
NSDateComponents *components = [[NSDateComponents alloc] init];
[components setMonth:3];
NSDate *expirationDate = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:[NSDate date] options:0];
[_keyStore setObject:expirationDate forKey:#"expirationDate"];
[_keyStore synchronize];
}
-(void) initializeStore {
[_keyStore setObject:nil forKey:#"expirationDate"];
[_keyStore synchronize];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#end
Please understand this is my first time trying this. What may seem stupid is still the learning phase for me. Thank you!
There is no runtime error in this story. All that's happened is that you've created an Exceptions breakpoint. Now you are pausing at it. Simply resume running. If this is troublesome, disable the Exceptions breakpoint.
Related
I have a quick question to a strange glitch in my In-App Purchase set up:
My ViewController has 2 buttons: A and B. When the user clicks one of these buttons, i am having my productID set to the chosen item. I then have a Buy button that the user should click to purchase the item. The issue I am having is that I can not seem to get the SKProductRequest to start the productsRequest method with the correct item.
In other words, the productRequest to Apple is always lagging 1 behind the correct product. Here is my code i have implemented:
ViewController.h
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
#interface ViewController : UIViewController <SKPaymentTransactionObserver, SKProductsRequestDelegate>
/// In app purchase crap
#property (strong, nonatomic) SKProduct *product;
#property (strong, nonatomic) SKProductsRequest *request;
#property (retain, nonatomic) NSString *productID;
#end
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
int start;
- (void)viewDidLoad {
[super viewDidLoad];
// Attempt to get request aligned with item selected.. :\
start = YES;
_productID = #"com.example.IAP.itemA";
[self Buy:self];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)Buy:(id)sender {
// Check for user to be able to make payment
if ([SKPaymentQueue canMakePayments]) {
_request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:_productID]];
_request.delegate = self;
[_request start];
} else {
NSLog(#"Please enable In App Purchasing in your settings");
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSArray *products = response.products;
if (products.count != 0) {
NSLog(#"Products available: %#", _product.localizedTitle);
_product = products[0];
if (start != YES) {
[self beginPaymentQueue];
}
else {
start = NO;
}
} else {
NSLog(#"Products not found.");
}
products = response.invalidProductIdentifiers;
for (SKProduct *product in products) {
NSLog(#"Product not found: %#", product);
}
}
- (void)beginPaymentQueue {
SKPayment *payment = [SKPayment paymentWithProduct:_product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased: { [self UnlockPurchase];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break; }
case SKPaymentTransactionStateFailed: { NSLog(#"Transaction Failed");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break; }
default:
break;
}
}
}
- (void)UnlockPurchase {
NSLog(#"Purchase Successful!");
}
// Item Buttons
- (IBAction)buttonA:(id)sender {
_productID = #"com.example.IAP.itemA";
}
- (IBAction)buttonB:(id)sender {
_productID = #"com.example.IAP.itemB";
}
Not sure what to do... I have tried calling the CanPurchase method in the viewDidLoad method, I have even tried switch statements inside the methods for the productID, but nothing worked.
If someone could suggest a solution or try and help I would be very appreciative. I have been trying to fix this for days. Thanks!
FYI: here is a log of what is happening ( I manually added comments on here for each line to let you know what the user has done to get that log in place of the full date)
app loads 12:14:49.488 IAP[4928:370136] Products available: (null)
click B then buy 12:14:52.053 IAP[4928:370136] Products available: IAP A
cancel iap 12:15:01.086 IAP[4928:370136] Transaction Failed
click B then buy 12:15:02.622 IAP[4928:370136] Products available: IAP B
cancel iap 12:15:04.256 IAP[4928:370136] Transaction Failed
click A then buy 12:15:02.622 IAP[4928:370136] Products available: IAP B
cancel iap 12:15:04.256 IAP[4928:370136] Transaction Failed
click A then buy 12:15:02.622 IAP[4928:370136] Products available: IAP A
cancel iap 12:15:04.256 IAP[4928:370136] Transaction Failed
That loop continues, always with the productRequest lagging behind by 1 with the chosen IAP. Please help!
It looks like you're just confusing yourself here:
NSArray *products = response.products;
if (products.count != 0) {
NSLog(#"Products available: %#", _product.localizedTitle);
_product = products[0];
You are getting the localized title from the product you stored earlier. This is why you are one behind. Instead, get the product and hence its title from the products that the response returned to you:
NSArray *products = response.products;
if (products.count != 0) {
SKProduct* product = products[0];
NSLog(#"Products available: %#", product.localizedTitle);
I create an iPhone application (with a view) / Watch (with interface) that displays speed, distance and a timer with a Play / Pause, Stop and Clear.
I used to share WatchConnectivity Application Context data between (Send and receipt of Context). Everything works so far, but I would add another page / interfaces on which exchanges Watch the Context with the iPhone and there I do not know at all how.
My Interface Storyboard
If I turn on the Session 2 interfaces, information is lost
Here is my current code, I want to toggle the display of labels TimeLabel, distanceLabel Label and speed on the second interface
//
// InterfaceController.m
// Watch Extension
//
// Created by Arnaud Roy on 25/10/2015.
// Copyright © 2015 Burotica. All rights reserved.
//
#import "InterfaceController.h"
#import <WatchConnectivity/WatchConnectivity.h>
#interface InterfaceController() <WCSessionDelegate>
#property (strong, nonatomic) WCSession *session;
#property (unsafe_unretained, nonatomic) IBOutlet WKInterfaceButton *startLabel;
#property (unsafe_unretained, nonatomic) IBOutlet WKInterfaceLabel *timeLabel;
#property (unsafe_unretained, nonatomic) IBOutlet WKInterfaceLabel *distanceLabel;
#property (unsafe_unretained, nonatomic) IBOutlet WKInterfaceLabel *vitesseLabel;
#property (nonatomic,assign) BOOL running;
#end
#implementation InterfaceController
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
// Configure interface objects here.
}
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
[super willActivate];
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
//NSLog(#"SESSION AVAIBLE");
}
//Objective-C
if ([[WCSession defaultSession] isReachable]) {
//NSLog(#"SESSION REACHABLE");
}
self.running = false;
}
- (void)didDeactivate {
// This method is called when watch view controller is no longer visible
[super didDeactivate];
}
- (IBAction)startButton {
if(self.running == false) {
[self sendCmd:#"start"];
[self.startLabel setTitle:[NSString stringWithFormat:#"PAUSE"]];
self.running = true;
}
else
{
[self sendCmd:#"pause"];
[self.startLabel setTitle:[NSString stringWithFormat:#"PLAY"]];
self.running = false;
}
}
- (IBAction)clearButton {
[self sendCmd:#"clear"];
}
- (IBAction)plusmoinsButton {
[self sendCmd:#"plusmoins"];
}
- (IBAction)stopButton {
[self sendCmd:#"stop"];
}
-(void)sendCmd:(NSString*) cmdSendW{
WCSession *session = [WCSession defaultSession];
NSError *error;
[session updateApplicationContext:#{#"cmdSendW":cmdSendW} error:&error];
}
- (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary<NSString *,id> *)applicationContext {
NSString *cmdSend = [applicationContext objectForKey:#"cmdSend"];
NSString *timeSend = [applicationContext objectForKey:#"timeSend"];
NSString *distanceSend = [applicationContext objectForKey:#"distanceSend"];
NSString *partielSend = [applicationContext objectForKey:#"partielSend"];
NSString *vitesseSend = [applicationContext objectForKey:#"vitesseSend"];
dispatch_async(dispatch_get_main_queue(), ^{
if([cmdSend isEqual: #"start"])
{
[self.startLabel setTitle:[NSString stringWithFormat:#"PAUSE"]];
[self.timeLabel setText:[NSString stringWithFormat:#"Time : %#", timeSend]];
[self.distanceLabel setText:[NSString stringWithFormat:#"Distance : %#", distanceSend]];
[self.vitesseLabel setText:[NSString stringWithFormat:#"Vitesse : %#", vitesseSend]];
self.running = true;
}
else if([cmdSend isEqual: #"pause"])
{
[self.startLabel setTitle:[NSString stringWithFormat:#"PLAY"]];
[self.timeLabel setText:[NSString stringWithFormat:#"Time : %#", timeSend]];
[self.distanceLabel setText:[NSString stringWithFormat:#"Distance : %#", distanceSend]];
[self.vitesseLabel setText:[NSString stringWithFormat:#"Vitesse : %#", vitesseSend]];
self.running = false;
}
else if([cmdSend isEqual: #"stop"])
{
[self.startLabel setTitle:[NSString stringWithFormat:#"PLAY"]];
[self.timeLabel setText:[NSString stringWithFormat:#"Time : %#", timeSend]];
[self.distanceLabel setText:[NSString stringWithFormat:#"Distance : %#", distanceSend]];
[self.vitesseLabel setText:[NSString stringWithFormat:#"Vitesse : %#", vitesseSend]];
self.running = false;
}
});
}
#end
Thanks for your help
I'd suggest having your UIApplicationDelegate and ExtensionDelegate create an instance of a new object you make that is the WCSessionDelegate. This object will persist incoming data to disk, and post notifications that the backing data store has been updated. Then each of the UI components can monitor for these notifications and update their UIs as appropriate.
I'm looking through the internet for days already and can not find solution to my problem. I have created an app and want you to remove the advertising through an in app purchase. My problem is that the app sometimes crashes when I click on the button "BuyProduct". I then get the error message "Thread 1: EXC_BAD_ACCESS (code = 1, address = 0x16e6f6980)" after the line [[SKPaymentQueue defaultQueue] addPayment:payment];.
Here my .h of my PurchasedViewController:
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
#interface PurchasedViewController2 : UIViewController <SKPaymentTransactionObserver, SKProductsRequestDelegate>{
NSTimer *myTimer;
int countdown;
}
#property (strong, nonatomic) SKProduct *product;
#property (strong, nonatomic) NSString *productID;
#property (strong, nonatomic) IBOutlet UILabel *productTitle;
#property (strong, nonatomic) IBOutlet UITextView *productDescription;
#property (strong, nonatomic) IBOutlet UIButton *buyButton;
#property (weak, nonatomic) IBOutlet UIButton *goBack;
#property (weak, nonatomic) IBOutlet UILabel *labelGray;
#property (weak, nonatomic) IBOutlet UILabel *labelCountdown;
- (IBAction)GoBack:(id)sender;
- (IBAction)BuyProduct:(id)sender;
- (IBAction)Restore:(id)sender;
-(void)getProductID:(UIViewController *)viewController;
-(void)UnlockPurchase;
-(void)update;
#end
here the .m
#import "PurchasedViewController2.h"
#import "ViewController.h"
#interface PurchasedViewController2 ()
#property (strong, nonatomic) ViewController *homeViewController;
#end
#implementation PurchasedViewController2
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
countdown = 5;
_labelCountdown.text = [NSString stringWithFormat:#"%i",countdown];
_goBack.enabled = FALSE;
myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(update) userInfo:nil repeats:YES];
// Do any additional setup after loading the view.
}
-(void)update{
countdown--;
if (countdown >=0) {
_labelCountdown.text = [NSString stringWithFormat:#"%i",countdown];
}
if (countdown <=0) {
_labelCountdown.hidden = TRUE;
_labelGray.hidden = TRUE;
_goBack.enabled = TRUE;
[myTimer invalidate];
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
- (IBAction)GoBack:(id)sender {
[self dismissViewControllerAnimated:YES completion:NULL];
}
- (IBAction)BuyProduct:(id)sender {
SKPayment *payment = [SKPayment paymentWithProduct:_product];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
-(void)viewDidUnload{
[[SKPaymentQueue defaultQueue]removeTransactionObserver:self];
[super viewDidUnload];
}
- (IBAction)Restore:(id)sender {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
[self UnlockPurchase];
}
-(void)getProductID:(ViewController *)viewController{
_homeViewController = viewController;
if ([SKPaymentQueue canMakePayments]) {
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:self.productID]];
request.delegate = self;
[request start];
}
else{
_productDescription.text = #"Please enable in app purchase in your setting";
}
}
#pragma mark _
#pragma mark SKProductsRequestDelegate
-(void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *products = response.products;
if (products.count != 0) {
_product = products[0];
_buyButton.enabled = YES;
_productTitle.text = _product.localizedTitle;
_productDescription.text = _product.localizedDescription;
}
else{
_productTitle.text = #"Product Not Found";
}
products = response.invalidProductIdentifiers;
for (SKProduct *product in products) {
NSLog(#"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{
_buyButton.enabled = NO;
[_buyButton setTitle:#"Purchased" forState:UIControlStateDisabled];
[_homeViewController Purchased];
}
#end
and here is my call of the view:
- (IBAction)PurchaseItem:(id)sender {
_purchaseController = [[PurchasedViewController2 alloc] initWithNibName:nil bundle:nil];
_purchaseController.productID =#"PS.PileUp.RemoveAds";
[[SKPaymentQueue defaultQueue] addTransactionObserver:_purchaseController];
[self presentViewController:_purchaseController animated:YES completion:NULL];
[_purchaseController getProductID:self];
}
Thank you very munch
Thx i solve it. I change the GoBack Method to:
- (IBAction)GoBack:(id)sender {
[[SKPaymentQueue defaultQueue]removeTransactionObserver:self];
[self dismissViewControllerAnimated:YES completion:NULL];
}
It looks like you have a race condition between pressing the BuyProduct button and creation of the _product. In general, you should be finished the SKProductsRequest before allowing the user to tap BuyProduct. That is, do you know _product has the value you want when BuyProduct is pressed?
I am getting my In-App Purchase Title (I can see it in NSLog), but the UILabel that shows the title gets called after viewDidLoad. Is there any way I can get the Title before viewDidLoad, so that the UILabel will show the Title?
Below is my code. Let me know if you need me to post anything more, thanks!
PurchaseViewController.h
#interface PurchaseViewController : UIViewController
<SKPaymentTransactionObserver, SKProductsRequestDelegate>
#property (strong, nonatomic) SKProduct *product;
#property (strong, nonatomic) NSString *productID;
#property (strong, nonatomic) IBOutlet UILabel *productTitle;
#property (strong, nonatomic) IBOutlet UIButton *buyButton;
#property (strong, nonatomic) IBOutlet UITextView *productDescription;
- (IBAction)buyProduct:(id)sender;
- (void)getProductInfo:(UIViewController *)viewController;
#property (nonatomic) int buttonNumber;
#end
PurchaseViewController.m
#pragma mark - SKProductsRequestDelegate
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSArray *products = response.products;
if (products.count !=0) {
_product = products[0];
//_buyButton.enabled = YES;
_productTitle.text = _product.localizedTitle;
_productDescription.text = _product.localizedDescription;
}
else {
_productTitle.text = #"Product not found";
}
products = response.invalidProductIdentifiers;
for (SKProduct *product in products) {
NSLog(#"Product not found: %#", product);
}
}
- (IBAction)buyProduct:(id)sender {
SKPayment *payment = [SKPayment paymentWithProduct:_product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
#pragma mark SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
[self unlockFeature];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
NSLog(#"Transaction Failed");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
default:
break;
}
}
}
The usual architecture is to get the product info before showing the view containing the info. Then you pass that info to your view controller as you create the view controller and show its view.
Moreover, you shouldn't handle these things in any view controller code. Fetching product infos and similar stuff like that (Location, Session, app's models' factories etc.) is a typical use case of shared data for a (optionally stateful) Manager singleton (f.e. PurchaseManager in this case) which would be called on app launch/AppDelegate methods and work in the background, independently on your UI code/threads.
Example:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[[PurchaseManager defaultManager] fetchProducts];
}
i have carried out these several steps for five non consumable in app purchases to be available within my app but still nothing is showing up when i go to the TableViewController that i linked the IAP code to... (i would also like to give a big thanks to the tutorial i followed which has got me this far by raywenderlich)
-Made a new app ID
-Successfully followed all the steps for linking and downloading certificates
Changed the bundle ID on xcode for my project to the one i made for
my app on IOS developer portal
-Linked all the devices and made a test user account
-Made the non consumable IAP on itunes connect and used the Identifiers within the IAP coding
-Signed my phone out of my itunes account so tht its ready to be used with a test account
-Coding for the IAP looks to be correct with store kit imported and product identifiers implemented within the .m file
-Waited 24 hours for the newly created IAP to sync with itunes connect
-I DIDNT upload the binaries!
i have hosting with apple turned on but didnt upload anything - could
this be the issue?
i deleted the app on my phone and reinstalled for testing but still
nothing
All i get when i successfull run the build is a page that looks like its loading something but then nothing a blank page that i can pull down to refresh on but still nothing no IAP that i created.
Any suggestions of what else i can do or add and if you need me to upload all the codes for all the files i can do...
coding for IAP within the ViewController.h file
#import "Accounts/Accounts.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#interface ViewController19 : UITableViewController
#end
Coding for IAP within the ViewController.m file
#import "DetailViewController.h"
#import "SecretsIAPHelper.h"
#import <StoreKit/StoreKit.h>
#interface ViewController19 () {
NSArray *_products;
NSNumberFormatter * _priceFormatter;
}
#end
#implementation ViewController19
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = #"XXXXXXXXXXXXX";
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:#selector(reload) forControlEvents:UIControlEventValueChanged];
[self reload];
[self.refreshControl beginRefreshing];
_priceFormatter = [[NSNumberFormatter alloc] init];
[_priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[_priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"Restore" style:UIBarButtonItemStyleBordered target:self action:#selector(restoreTapped:)];
}
- (void)restoreTapped:(id)sender {
[[SecretsIAPHelper sharedInstance] restoreCompletedTransactions];
}
- (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 {
NSString * productIdentifier = notification.object;
[_products enumerateObjectsUsingBlock:^(SKProduct * product, NSUInteger idx, BOOL *stop) {
if ([product.productIdentifier isEqualToString:productIdentifier]) {
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:idx inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
*stop = YES;
}
}];
}
- (void)reload {
_products = nil;
[self.tableView reloadData];
[[SecretsIAPHelper 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;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _products.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
SKProduct * product = (SKProduct *) _products[indexPath.row];
cell.textLabel.text = product.localizedTitle;
[_priceFormatter setLocale:product.priceLocale];
cell.detailTextLabel.text = [_priceFormatter stringFromNumber:product.price];
if ([[SecretsIAPHelper 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;
}
- (void)buyButtonTapped:(id)sender {
UIButton *buyButton = (UIButton *)sender;
SKProduct *product = _products[buyButton.tag];
NSLog(#"Buying %#...", product.productIdentifier);
[[SecretsIAPHelper sharedInstance] buyProduct:product];
}
#end
Coding for IAP within the IAPHelper.h file
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
UIKIT_EXTERN NSString *const IAPHelperProductPurchasedNotification;
typedef void (^RequestProductsCompletionHandler)(BOOL success, NSArray * products);
#interface IAPHelper : NSObject
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler;
- (void)buyProduct:(SKProduct *)product;
- (BOOL)productPurchased:(NSString *)productIdentifier;
- (void)restoreCompletedTransactions;
#end
Coding for IAP within the IAPHelper.m file
#import "IAPHelper.h"
#import <StoreKit/StoreKit.h>
NSString *const IAPHelperProductPurchasedNotification = #"IAPHelperProductPurchasedNotification";
// 2
#interface IAPHelper () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
#end
// 3
#implementation IAPHelper {
SKProductsRequest * _productsRequest;
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);
} else {
NSLog(#"Not purchased: %#", productIdentifier);
}
}
// Add self as transaction observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler {
// 1
_completionHandler = [completionHandler copy];
// 2
_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
_productsRequest.delegate = self;
[_productsRequest start];
}
- (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];
}
#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
{
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];
}
- (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);
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void)provideContentForProductIdentifier:(NSString *)productIdentifier {
[_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];
}
#end
Coding for IAP within the SecretsIAPHelper.h file
#import "IAPHelper.h"
#interface SecretsIAPHelper : IAPHelper
+ (SecretsIAPHelper *)sharedInstance;
#end
Coding for IAP within the SecretsIAPHelper.m file
#import "SecretsIAPHelper.h"
#implementation SecretsIAPHelper
+ (SecretsIAPHelper *)sharedInstance {
static dispatch_once_t once;
static SecretsIAPHelper * sharedInstance;
dispatch_once(&once, ^{
NSSet * productIdentifiers = [NSSet setWithObjects:
#"com.designsbydeondrae.XXXXXXX.remove_ads",
#"com.designsbydeondrae.XXXXXXX.FoundationSkills",
#"com.designsbydeondrae.XXXXXXX.IntermediateSkills",
#"com.designsbydeondrae.XXXXXXX.AllSkills",
#"com.designsbydeondrae.XXXXXXX.AdvancedSkills",
nil];
sharedInstance = [[self alloc] initWithProductIdentifiers:productIdentifiers];
});
return sharedInstance;
}
#end
Coding for IAP within the DetailViewController.h file
#import <UIKit/UIKit.h>
#interface DetailViewController : UIViewController
#property (strong, nonatomic) id detailItem;
#property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
#end
Coding for IAP within the DetailViewController.h file
#import "DetailViewController.h"
#interface DetailViewController ()
- (void)configureView;
#end
#implementation DetailViewController
#pragma mark - Managing the detail item
- (void)setDetailItem:(id)newDetailItem
{
if (_detailItem != newDetailItem) {
_detailItem = newDetailItem;
// Update the view.
[self configureView];
}
}
- (void)configureView
{
// Update the user interface for the detail item.
if (self.detailItem) {
self.detailDescriptionLabel.text = [self.detailItem description];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self configureView];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
So just to summarise...
Used StoreKit to access the In-App Purchase APIs and retrieve the
list which brings up nothing when i run the build
Specified the product identifiers for my app
Displaying the products which seems to be an issue though it does
show a screen like its about to load something then nothing just a
page with a pull to refresh on it
For any one that finds this post in the future, the reason no items where showing up when requesting IAP was due to content not being uploaded for hosted content.