There are plenty of IAP (In App Purchase) tutorials in the internet. BUT, all of them are damn complicated. None of the tutorials (that I found anyway) that shows the easiest way to implement IAP. If that is how you feel, read on....
For example, over at Ray Wenderlich (
this tutorial), you can try read it, unless you are very familiar with classes and what nots, you'd end up like me - giving up to make IAP in your app. Don't get me wrong, Ray Wenderlich provides one of the TOP NOTCH tuts, but sometimes they just speak a different language than us, noobies and amateur coders.
So I thought, since it is my first time to implement IAP in one of my apps, (I never had an IAP app before), might as well I try find my own way of putting an IAP which is EASY, STRAIGHTFORWARD, and FAST TO IMPLEMENT.
Lets get started.
Firstly, for this tutorial, I am going to assume a few things:
1. that you want to implement IAP by way of "feature" buttons.
2. that you do not have too many items to be purchased (if you do have many IAP items,
then it is probably best to use UITableView as in Ray Wenderlich's sample)
3. that all IAP items/features are built in into your app bundle (ie, you do not have a downloadable IAP items, etc)
If these assumptions are what you are doing, then read on! For this tutorial example project, I'll just put 2 buttons on a single view app. One button is an example of a readily available feature and another button is another feature that requires an IAP to use.
So, as normal, go to your XCode, create a project and goto the XIB file and put 2 buttons on it. For the 2nd button, you need to put an "indicator" that the feature is kinda locked. Here, I use a padlock icon as the background image to illustrate this. If you use button of the type Custom, then you could set the button's Image property as the padlock icon and the background property as your button appearance. When the app is ran, here's what it looks like:
I also added a UILabel at the bottom just to do something when you press the feature button. For this example, I am going to make the label say "Feature 1" when you press button 1 and say "Feature 2" when you press button 2.
But since Feature 2 is a IAP item, we need to check if user has purchased the feature before showing "Feature 2". Anyway, lets make the basic functions first.
Declare 2 IBActions for the 2 buttons. And also declare IBOutlets for the IAP Feature button and for the UILabel in .h. Also declare a UIAlertView so that we can check for the user response in this alert later. Remember to synthesize both objects in your .m file.
Your .h should look something like this:
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
@interface ViewController : UIViewController {
IBOutlet UIButton *feature2Btn;
IBOutlet UILabel *featureLabel;
UIAlertView *askToPurchase;
}
@property (nonatomic, retain) UIButton *feature2Btn;
@property (nonatomic, retain) UILabel *featureLabel;
-(IBAction)doFeature1:(id)sender;
-(IBAction)doFeature2:(id)sender;
@end
Now open XIB file and connect all the necessary links. Connect IBActions to Touch Up Inside of File Owner, and both IBOutlets. Note that we need IBOutlet of Feature 2 button because we are going to change the Lock icon condition later on. Having the button declared will make things easier later.
Next, open .m file and write the methods for the button taps.
-(IBAction)doFeature1:(id)sender {
featureLabel.text = @"Feature 1";
}
-(IBAction)doFeature2:(id)sender {
featureLabel.text = @"Feature 2";
}
Now run the app, both buttons should be working now when you tap it, the UILabel below will change according to which button you tap! Congratulations. No, you are not done. We want to lock Feature 2 now, so tapping button "2" should check for IAP first before allowing user to use it.
For this purpose, we will create another method to check for IAP item somewhere.... Earlier, when I was learning how to implement IAP, I thought of using NSUserDefaultsto store some values (like a BOOL) so that we can set that flag if user completed an IAP. However, there is a problem with this, while it is simple, if user deleted the app, and redownload it later, the IAP flag will be gone forever - NSUserDefaults is stored as .plist file in the APP BUNDLE folder.... means user who already bought the IAP, needs to purchase the IAP again = angry users. Grr..
A better way to handle this is to make use of the KEYCHAIN. Now, we can learn to implement Keychain, but why reinvent the wheel? Head over
HERE to download the KeyChain wrapper. After you downloaded it copy and include both SFHFKeychainUtils.h and .m into your project. (I've already included the files in the Sample project of this tutorial, but do download the latest version for your app).
What is Keychain?Keychain is something like Registry in Windows. It is a more secure place to store sensitive information in the iDevice and it is not tied to the app bundle.
In your .m file, remember to #import "SFHFKeychainUtils.h". We also need to add the "Security.framework" to our project. After that, lets create the method to check the IAP item in the keychain.
-(BOOL)IAPItemPurchased {
NSError *error = nil;
NSString *password = [SFHFKeychainUtils getPasswordForUsername:@"IAPNoob01" andServiceName:kStoredData error:&error];
if ([password isEqualToString:@"whatever"]) return YES; else return NO;
}
We are just simply using the username/password saving feature of KeyChain utility to save our IAP item data. Nothing fancy. Also notice we are declaring this function as a BOOL, so it returns a YES or NO immediately when we call for it. Note that all 3 data here: username, password and ServiceName are developer defined. You choose what you want for those keys (they are all NSStrings). It does not matter what they are, as long as we can check for the "password".
Now, lets implement the lock feature of Feature 2. Modify the doFeature2 method as below:
-(IBAction)doFeature2:(id)sender {
if ([self IAPItemPurchased]) {
featureLabel.text = @"Feature 2";
} else {
// not purchased so show a view to prompt for purchase
askToPurchase = [[UIAlertView alloc]
initWithTitle:@"Feature 2 Locked"
message:@"Purchase Feature 2?"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Yes", @"No", nil];
askToPurchase.delegate = self;
[askToPurchase show];
[askToPurchase release];
}
}
What we do here is check if the IAP item flag is available in the keychain, if it exist then user has purchased Feature 2 before, so we proceed with it's function. If not, prompt user to buy Feature 2. In this example, I just use a simple AlertView. You can code a pretty UIView with colorful images and so on - entirely up to you.
Now lets handle the alertview feedbacks. If user chose to purchase, then we start the IAP request.
Before we go on to that, lets create an IAP item in iTunesConnect first.
Creating/Registering IAP Items in iTunesConnect.
Logon to your iTC account and click Manage Applications. Then click your app icon, and at the top right hand corner, click "Manage In-App Purchases". On the top left side, there is a Create New button. Click it. Click on Non Consumable. (Non consumable is a purchase type where user only needs to pay for a feature just one time.) Key In Ref Names and other details as shown in the pic below:
As for the screenshot, just upload a Dummy image of size 960x640 and you'll be able to save the IAP item. But you have to remember to update this screenshot prior to submission. The key info in this form is the Product ID. In this case
com.emirbytes.IAPNoob.01 , this is what we will be calling from our app later. Once we are done we can then create Test Accounts to test the IAP later. In the main page of iTunesConnect, go to Manage Users, and click on Test User and add a test user. Once you are done, you can logoff and return to coding.
Implementing StoreKit
The API that will help us implement the IAP is called StoreKit. So obviously
we need to add the StoreKit.framework to our project. Next
we need to import StoreKit.h and implement the related delegates, so modify the .h file as below:
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
@interface ViewController : UIViewController <SKProductsRequestDelegate, SKPaymentTransactionObserver> {
IBOutlet UIButton *feature2Btn;
IBOutlet UILabel *featureLabel;
UIAlertView *askToPurchase;
}
@property (nonatomic, retain) UIButton *feature2Btn;
@property (nonatomic, retain) UILabel *featureLabel;
-(IBAction)doFeature1:(id)sender;
-(IBAction)doFeature2:(id)sender;
@end
Since we use AlertView to prompt the user, we need to input the AlertView delegate as well, so add the UIAlertViewDelegate in .h file and implement the delegate function as below:
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView==askToPurchase) {
if (buttonIndex==0) {
// user tapped YES, but we need to check if IAP is enabled or not.
if ([SKPaymentQueue canMakePayments]) {
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:@"com.emirbytes.IAPNoob.01"]];
request.delegate = self;
[request start];
} else {
UIAlertView *tmp = [[UIAlertView alloc]
initWithTitle:@"Prohibited"
message:@"Parental Control is enabled, cannot make a purchase!"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Ok", nil];
[tmp show];
[tmp release];
}
}
}
}
First we check if the alertview is the IAP prompt alertview, if it is then we check if Yes button was pressed, and if it is, then we check "canMakePayments". This function will return YES or NO depending on whether the In-App Purchases Settings in the device is set to On or Off (Settings App -> General -> Restrictions). This way, the payment is allowable only if this restriction is not set (to prevent childrens from simply buying your IAPs unintentionally). And, if we can make IAP request, initiate the request with the Product ID that we specified earlier in iTC.
Since IAP requires internet connection and takes some time (a few seconds) to be processed, at this point it is a good idea to check for internet connection availability and to display a Wait View (I do not include it for the sake of simplicity). For easier demo, I put another UILabel to show the status of purchase.
Once we request this IAP to the IAP server, the app will then wait for a reply from the IAP server and fires off the StoreKit delegates:
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
-(void)requestDidFinish:(SKRequest *)request
-(void)request:(SKRequest *)request didFailWithError:(NSError *)error
The important delegates are the first two that are in bold. Right after we request a product via SKProductRequest in alertView delegate above, the first delegate that is going to respond is the didReceiveResponse delegate. Here, it will check whether the Product ID we requested is available or not, and if available, do the payment (Ka Ching!)...
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
// remove wait view here
SKProduct *validProduct = nil;
int count = [response.products count];
if (count>0) {
validProduct = [response.products objectAtIndex:0];
SKPayment *payment = [SKPayment paymentWithProductIdentifier:@"com.emirbytes.IAPNoob.01"];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:payment]; // <-- KA CHING!
} else {
UIAlertView *tmp = [[UIAlertView alloc]
initWithTitle:@"Not Available"
message:@"No products to purchase"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Ok", nil];
[tmp show];
[tmp release];
}
}
After we request a payment, the next delegate that is going to respond is the updatedTransactions delegate, where we check for transactions type: there are 4 types that must be handled properly.
SKPaymentTransactionStatePurchasing - indicates still processing the purchasing - display a wait view.
SKPaymentTransactionStatePurchased - indicates purchase completed - unlock features by code
SKPaymentTransactionStateRestored - purchase restored from an interrupt (phone call etc)
SKPaymentTransactionStateFailed - purchase failed - show alert
I will explain the code in StatePurchased condition:
case SKPaymentTransactionStatePurchased:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
// remove wait view and unlock feature 2
UIAlertView *tmp = [[UIAlertView alloc]
initWithTitle:@"Complete"
message:@"You have unlocked Feature 2!"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Ok", nil];
[tmp show];
[tmp release];
NSError *error = nil;
[SFHFKeychainUtils storeUsername:@"IAPNoob01" andPassword:@"whatever" forServiceName:kStoredData updateExisting:YES error:&error];
// apply purchase action - hide lock overlay and
[feature2Btn setBackgroundImage:nil forState:UIControlStateNormal];
// do other thing to enable the features
break;
First, in above case, we tell the server to finish the transaction, then we show to user that the Feature has been unlocked. Then, we register a flag in Keychain with the specific username, password and servicename (remember they are case sensitive). Finally we remove any Padlock icon and other visual settings.
IMPORTANT: YOU CAN ONLY TEST IN-APP PURCHASE IN YOUR DEVICE, AND NOT IN SIMULATOR. USE THE DEVELOPMENT PROVISION TO TEST IT. AND MAKE SURE YOU LOG OFF YOUR OWN APPLE ID and USE THE TEST USER ID TO DO TEST THE IAP IN SANDBOX ENVIRONMENT! see additional notes.
We are almost done. Finally we need to re-check the purchase in the Keychain everytime the user activate the app. In this case I put it in viewDidLoad - here we call the checking function we created earlier - see how it is useful to have?:
if ([self IAPItemPurchased]) {
[feature2Btn setBackgroundImage:nil forState:UIControlStateNormal];
} else {
[feature2Btn setBackgroundImage:[UIImage imageNamed:@"Locked.png"] forState:UIControlStateNormal];
}
Here are some screenshots sequence of the IAP in action:
____________________________________
Oops! I forgot one more thing!
Since you will be testing this in your device, you might want to test the IAP a few times. So we need to add another button (which will be hidden or just deleted when you submit the app) to reset the Keychain value.
Just add a button... and implement IBAction for it as below:
-(IBAction)deleteKeyChain:(id)sender {
NSError *error = nil;
[SFHFKeychainUtils deleteItemForUsername:@"IAPNoob01" andServiceName:kStoredData error:&error];
}
To test the IAP process again, just tap the button and delete the app in your device, and then rerun it. The app's Feature 2 should be Locked again.
Ok, now we're Done! Yay!
ADDITIONAL NOTES:
Some readers have given me some feedback about how they get a "No Products" alert when running it on the device. Please check that you have assigned Code Signing to the certificates correctly (both development and Release provision profiles). Things you can try:
Reset your app - delete the app in sim, do Clean All in XCode, double check your provisioning profiles (reinstall them if necessary). Remember that bundle ID are case sensitive. Ensure to logout your iTunes Store account in Sim/Device and make sure you login with a test account to test the IAP. Check everything in detail.
Also, I have verified that you can test the IAP in Simulator as well (I tested it using XCode 4.3.1 and Simulator 5.1). Thanks all for your feedbacks!