261

How do you add an in-app purchase to an iOS app? What are all the details and is there any sample code?

This is meant to be a catch-all of sorts for how to add in-app purchases to iOS apps

Jojodmo
  • 22,792
  • 13
  • 59
  • 99
  • 13
    What about reading the "In-App Purchase Programming Guide"? – rmaddy Oct 24 '13 at 04:24
  • 3
    Apple's [In-App Purchase Programming Guide](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html) – Hemang May 21 '14 at 13:57

5 Answers5

563

Swift Users

Swift users can check out My Swift Answer for this question.
Or, check out Yedidya Reiss's Answer, which translates this Objective-C code to Swift.

Objective-C Users

The rest of this answer is written in Objective-C

App Store Connect

  1. Go to appstoreconnect.apple.com and log in
  2. Click My Apps then click the app you want do add the purchase to
  3. Click the Features header, and then select In-App Purchases on the left
  4. Click the + icon in the middle
  5. For this tutorial, we are going to be adding an in-app purchase to remove ads, so choose non-consumable. If you were going to send a physical item to the user, or give them something that they can buy more than once, you would choose consumable.
  6. For the reference name, put whatever you want (but make sure you know what it is)
  7. For product id put tld.websitename.appname.referencename this will work the best, so for example, you could use com.jojodmo.blix.removeads
  8. Choose cleared for sale and then choose price tier as 1 (99¢). Tier 2 would be $1.99, and tier 3 would be $2.99. The full list is available if you click view pricing matrix I recommend you use tier 1, because that's usually the most anyone will ever pay to remove ads.
  9. Click the blue add language button, and input the information. This will ALL be shown to the customer, so don't put anything you don't want them seeing
  10. For hosting content with Apple choose no
  11. You can leave the review notes blank FOR NOW.
  12. Skip the screenshot for review FOR NOW, everything we skip we will come back to.
  13. Click 'save'

It could take a few hours for your product ID to register in App Store Connect, so be patient.

Setting up your project

Now that you've set up your in-app purchase information on App Store Connect, go into your Xcode project, and go to the application manager (blue page-like icon at the top of where your methods and header files are) click on your app under targets (should be the first one) then go to general. At the bottom, you should see linked frameworks and libraries click the little plus symbol and add the framework StoreKit.framework If you don't do this, the in-app purchase will NOT work!

If you are using Objective-C as the language for your app, you should skip these five steps. Otherwise, if you are using Swift, you can follow My Swift Answer for this question, here, or, if you prefer to use Objective-C for the In-App Purchase code but are using Swift in your app, you can do the following:

  1. Create a new .h (header) file by going to File > New > File... (Command ⌘ + N). This file will be referred to as "Your .h file" in the rest of the tutorial

  2. When prompted, click Create Bridging Header. This will be our bridging header file. If you are not prompted, go to step 3. If you are prompted, skip step 3 and go directly to step 4.

  3. Create another .h file named Bridge.h in the main project folder, Then go to the Application Manager (the blue page-like icon), then select your app in the Targets section, and click Build Settings. Find the option that says Swift Compiler - Code Generation, and then set the Objective-C Bridging Header option to Bridge.h

  4. In your bridging header file, add the line #import "MyObjectiveCHeaderFile.h", where MyObjectiveCHeaderFile is the name of the header file that you created in step one. So, for example, if you named your header file InAppPurchase.h, you would add the line #import "InAppPurchase.h" to your bridge header file.

  5. Create a new Objective-C Methods (.m) file by going to File > New > File... (Command ⌘ + N). Name it the same as the header file you created in step 1. For example, if you called the file in step 1 InAppPurchase.h, you would call this new file InAppPurchase.m. This file will be referred to as "Your .m file" in the rest of the tutorial.

Coding

Now we're going to get into the actual coding. Add the following code into your .h file:

BOOL areAdsRemoved;

- (IBAction)restore;
- (IBAction)tapsRemoveAds;

Next, you need to import the StoreKit framework into your .m file, as well as add SKProductsRequestDelegate and SKPaymentTransactionObserver after your @interface declaration:

#import <StoreKit/StoreKit.h>

//put the name of your view controller in place of MyViewController
@interface MyViewController() <SKProductsRequestDelegate, SKPaymentTransactionObserver>

@end

@implementation MyViewController //the name of your view controller (same as above)
  //the code below will be added here
@end

and now add the following into your .m file, this part gets complicated, so I suggest that you read the comments in the code:

//If you have more than one in-app purchase, you can define both of
//of them here. So, for example, you could define both kRemoveAdsProductIdentifier
//and kBuyCurrencyProductIdentifier with their respective product ids
//
//for this example, we will only use one product

#define kRemoveAdsProductIdentifier @"put your product id (the one that we just made in App Store Connect) in here"

- (IBAction)tapsRemoveAds{
    NSLog(@"User requests to remove ads");

    if([SKPaymentQueue canMakePayments]){
        NSLog(@"User can make payments");
    
        //If you have more than one in-app purchase, and would like
        //to have the user purchase a different product, simply define 
        //another function and replace kRemoveAdsProductIdentifier with 
        //the identifier for the other product

        SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:kRemoveAdsProductIdentifier]];
        productsRequest.delegate = self;
        [productsRequest start];
    
    }
    else{
        NSLog(@"User cannot make payments due to parental controls");
        //this is called the user cannot make payments, most likely due to parental controls
    }
}

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    SKProduct *validProduct = nil;
    int count = [response.products count];
    if(count > 0){
        validProduct = [response.products objectAtIndex:0];
        NSLog(@"Products Available!");
        [self purchase:validProduct];
    }
    else if(!validProduct){
        NSLog(@"No products available");
        //this is called if your product id is not valid, this shouldn't be called unless that happens.
    }
}

- (void)purchase:(SKProduct *)product{
    SKPayment *payment = [SKPayment paymentWithProduct:product];

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

- (IBAction) restore{
    //this is called when the user restores purchases, you should hook this up to a button
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

- (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");

            //if you have more than one in-app purchase product,
            //you restore the correct product for the identifier.
            //For example, you could use
            //if(productID == kRemoveAdsProductIdentifier)
            //to get the product identifier for the
            //restored purchases, you can use
            //
            //NSString *productID = transaction.payment.productIdentifier;
            [self doRemoveAds];
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
        }
    }   
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    for(SKPaymentTransaction *transaction in transactions){
        //if you have multiple in app purchases in your app,
        //you can get the product identifier of this transaction
        //by using transaction.payment.productIdentifier
        //
        //then, check the identifier against the product IDs
        //that you have defined to check which product the user
        //just purchased            

        switch(transaction.transactionState){
            case SKPaymentTransactionStatePurchasing: NSLog(@"Transaction state -> Purchasing");
                //called when the user is in the process of purchasing, do not add any of your own code here.
                break;
            case SKPaymentTransactionStatePurchased:
            //this is called when the user has successfully purchased the package (Cha-Ching!)
                [self doRemoveAds]; //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                NSLog(@"Transaction state -> Purchased");
                break;
            case SKPaymentTransactionStateRestored:
                NSLog(@"Transaction state -> Restored");
                //add the same code as you did from SKPaymentTransactionStatePurchased here
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                //called when the transaction does not finish
                if(transaction.error.code == SKErrorPaymentCancelled){
                    NSLog(@"Transaction state -> Cancelled");
                    //the user cancelled the payment ;(
                }
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
        }
    }
}

Now you want to add your code for what will happen when the user finishes the transaction, for this tutorial, we use removing adds, you will have to add your own code for what happens when the banner view loads.

- (void)doRemoveAds{
    ADBannerView *banner;
    [banner setAlpha:0];
    areAdsRemoved = YES;
    removeAdsButton.hidden = YES;
    removeAdsButton.enabled = NO;
    [[NSUserDefaults standardUserDefaults] setBool:areAdsRemoved forKey:@"areAdsRemoved"];
    //use NSUserDefaults so that you can load whether or not they bought it
    //it would be better to use KeyChain access, or something more secure
    //to store the user data, because NSUserDefaults can be changed.
    //You're average downloader won't be able to change it very easily, but
    //it's still best to use something more secure than NSUserDefaults.
    //For the purpose of this tutorial, though, we're going to use NSUserDefaults
    [[NSUserDefaults standardUserDefaults] synchronize];
}

If you don't have ads in your application, you can use any other thing that you want. For example, we could make the color of the background blue. To do this we would want to use:

- (void)doRemoveAds{
    [self.view setBackgroundColor:[UIColor blueColor]];
    areAdsRemoved = YES
    //set the bool for whether or not they purchased it to YES, you could use your own boolean here, but you would have to declare it in your .h file

    [[NSUserDefaults standardUserDefaults] setBool:areAdsRemoved forKey:@"areAdsRemoved"];
    //use NSUserDefaults so that you can load wether or not they bought it
    [[NSUserDefaults standardUserDefaults] synchronize];
}

Now, somewhere in your viewDidLoad method, you're going to want to add the following code:

areAdsRemoved = [[NSUserDefaults standardUserDefaults] boolForKey:@"areAdsRemoved"];
[[NSUserDefaults standardUserDefaults] synchronize];
//this will load wether or not they bought the in-app purchase

if(areAdsRemoved){
    [self.view setBackgroundColor:[UIColor blueColor]];
    //if they did buy it, set the background to blue, if your using the code above to set the background to blue, if your removing ads, your going to have to make your own code here
}

Now that you have added all the code, go into your .xib or storyboard file, and add two buttons, one saying purchase, and the other saying restore. Hook up the tapsRemoveAds IBAction to the purchase button that you just made, and the restore IBAction to the restore button. The restore action will check if the user has previously purchased the in-app purchase, and give them the in-app purchase for free if they do not already have it.

Submitting for review

Next, go into App Store Connect, and click Users and Access then click the Sandbox Testers header, and then click the + symbol on the left where it says Testers. You can just put in random things for the first and last name, and the e-mail does not have to be real - you just have to be able to remember it. Put in a password (which you will have to remember) and fill in the rest of the info. I would recommend that you make the Date of Birth a date that would make the user 18 or older. App Store Territory HAS to be in the correct country. Next, log out of your existing iTunes account (you can log back in after this tutorial).

Now, run your application on your iOS device, if you try running it on the simulator, the purchase will always error, you HAVE TO run it on your iOS device. Once the app is running, tap the purchase button. When you are prompted to log into your iTunes account, log in as the test user that we just created. Next,when it asks you to confirm the purchase of 99¢ or whatever you set the price tier too, TAKE A SCREEN SNAPSHOT OF IT this is what your going to use for your screenshot for review on App Store Connect. Now cancel the payment.

Now, go to App Store Connect, then go to My Apps > the app you have the In-app purchase on > In-App Purchases. Then click your in-app purchase and click edit under the in-app purchase details. Once you've done that, import the photo that you just took on your iPhone into your computer, and upload that as the screenshot for review, then, in review notes, put your TEST USER e-mail and password. This will help apple in the review process.

After you have done this, go back onto the application on your iOS device, still logged in as the test user account, and click the purchase button. This time, confirm the payment Don't worry, this will NOT charge your account ANY money, test user accounts get all in-app purchases for free After you have confirmed the payment, make sure that what happens when the user buys your product actually happens. If it doesn't, then thats going to be an error with your doRemoveAds method. Again, I recommend using changing the background to blue for testing the in-app purchase, this should not be your actual in-app purchase though. If everything works and you're good to go! Just make sure to include the in-app purchase in your new binary when you upload it to App Store Connect!


Here are some common errors:

Logged: No Products Available

This could mean four things:

  • You didn't put the correct in-app purchase ID in your code (for the identifier kRemoveAdsProductIdentifier in the above code
  • You didn't clear your in-app purchase for sale on App Store Connect
  • You didn't wait for the in-app purchase ID to be registered in App Store Connect. Wait a couple hours from creating the ID, and your problem should be resolved.
  • You didn't complete filling your Agreements, Tax, and Banking info.

If it doesn't work the first time, don't get frustrated! Don't give up! It took me about 5 hours straight before I could get this working, and about 10 hours searching for the right code! If you use the code above exactly, it should work fine. Feel free to comment if you have any questions at all.

I hope this helps to all of those hoping to add an in-app purchase to their iOS application. Cheers!

Community
  • 1
  • 1
Jojodmo
  • 22,792
  • 13
  • 59
  • 99
  • 1
    but if i not add that line, when i click on restore button nothing happen.. anyway thank you very much for this tutorial ;) – Ilario Jan 13 '14 at 08:52
  • I would suggest that you save the settings that the ad has been removed in the keychain. The `NSUserDefault` is really easy to edit with some simple knowledge. – rckoenes Feb 20 '14 at 15:59
  • @Jojodmo2010 This is a superb tutorial thank you :) It has helped me add in app purchase to my app. Although I do agree with rckoenes that the Keychain should be used instead of NSUserDefault – Supertecnoboff Feb 25 '14 at 15:01
  • @Jojodmo2010 By the way for the if function surely it would make sense to test for this: "areAdsRemoved != 1" because you're if statement just checks if areAdsRemoved exists.... Does it not??? – Supertecnoboff Feb 25 '14 at 21:05
  • works totally fine for me. @Jojodmo2010 thx you so much for this intro! – zero3nna Mar 12 '14 at 13:37
  • @Jojodmo Actually, I didn't say anything about creating the test account, I spoke of the errors in the answer AFTER the test account is created. – Naseiva Khan Mar 17 '14 at 14:53
  • @jojodmo , i am sorry i didn't mean to offend but i am trying to use your tutorial of which i already marked up because i thought you had put a great amount of work in it. I found some confusion when trying to use it because it doubles up the purchase action. – clive dancey Apr 14 '14 at 22:07
  • I am not using any .Xib or storyboard(I am using cocos2d-x). So how to make restore and purchase button. Now I am doing this through removeads buttons when he click on directly it is asking to buy the product. Can you please tell me how can I do it. – socrates Nov 05 '14 at 09:37
  • 1
    "if(_*transaction*_ == SKPaymentTransactionStateRestored){ " should be if(_*transaction.transactionState*_ == SKPaymentTransactionStateRestored){ – Massmaker Mar 30 '15 at 12:57
  • 13
    Apple's best practices recommend you add the transaction observer to the AppDelegate, not the view controller actions. https://developer.apple.com/library/ios/technotes/tn2387/_index.html – Craig Pickering Apr 18 '15 at 09:30
  • 1
    This is a great tutorial, thanks. In my opinion it's also worth listening to the [Optimizing In-App Purchases WWDC 2014 video](https://developer.apple.com/videos/wwdc/2014/#303) which helps put some of the steps in perspective. – Kurt Anderson Apr 23 '15 at 17:08
  • 3
    I'm getting a count of 0 products, but I already checked the 3 possible reasons you listed. The only thing that comes to mind if that I haven't set up the contact info, bank info and tax info on the "ios paid app contract" inside itunes connect, could this be the reason? – Christopher Francisco Apr 25 '15 at 15:39
  • There's an Apple official sample code [StoreKitSuite](https://developer.apple.com/library/prerelease/ios/samplecode/sc1991/Introduction/Intro.html) which I think will be a great help. – Jaybo Apr 30 '15 at 09:36
  • I am about to implement the same, but i see this answer uses nsuserdefaults - idc about cheating etc, but if user removes the app and then installs it again he will have to buy it again? is there a way to check if the user already bought it ? – ColdSteel May 10 '15 at 22:58
  • @Jojodmo Oh thats the method... Thank you very much :) – ColdSteel May 11 '15 at 00:43
  • - (IBAction)purchase; - (IBAction)restore; - (IBAction)tapsRemoveAdsButton; BUt there is no tapsRemoveAdsButton method in .m file...and when I connect IBAction to purcahse my app crashes – Dalvik Jun 20 '15 at 09:25
  • @Jojodmo thanx for replying. you left one more thing..please change **(void)** to **(IBActction)** in your code.... and i have submited my app on saturday for in-APP-Purchase..and now i am getting product not availalbe..what can i do for it...i am running it on simulaotr – Dalvik Jun 22 '15 at 06:07
  • @Jojodmo I am getting "No products available" Logged. I checked my bundlie id in ituensconnect it is same.. what i can do to resolve this – Dalvik Jun 25 '15 at 08:02
  • @Jojodmo I haven't added any bank details as it is my test app...is it okay ? or i have to add all the details in order to test the app? – Dalvik Jun 26 '15 at 07:17
  • I never do a in-app Purchase before. I probably miss something, but i don't find in In-App Purchase where i need to set "non-consumable" and the price(i think is with new itunes connect, steps was change). And where need to go for create a test User. I really don't find the right place. – Gaby Fitcal Jul 22 '15 at 11:24
  • I create a test Account in Itunes Connect -> Users & Roles -> add new one, I give him Admin, but when i try connect with app in itunes connect , doesn't work. Account works good when i'm trying connect from mac, or iphone settings in App store. Can you help me? – Gaby Fitcal Jul 23 '15 at 12:43
  • I came again here with other question. How can i use your code for two different in-app purchase. First for remove ADS and second for buy something in app. I already created this two option in-app purchase. But i don't know how can i tell which method to be execute between both option. – Gaby Fitcal Aug 25 '15 at 07:35
  • @GabyFitcal I updated the answer adding how to do this. You can just define both of the product identifiers, and then create another function like `tapsRemoveAds`, but replace `kRemoveAdsProductIdentifier` with the defined product identifier for the other IAP when initializing the `productsRequest` variable – Jojodmo Aug 25 '15 at 21:39
  • Great summary. I have a problem where if user cancels form the enter iTunes password prompt then the request retires 2 more times then finally errors. My code is exactly as summarized above but I cant seem to trap the cancel condition. Any thoughts? – Fracdroid Sep 09 '15 at 17:08
  • 5
    You should explain that in Step 9, the Display name is what gets presented to the user. And it gets presented in this way: "Do you want to buy one DISPLAY NAME for $0.99?". This is important because I made my display name "Remove ads" and then my app got rejected because I was using improper grammar in the pop up! I had to change my Display Name to "Ad Removal Package". – Alan Scarpa Oct 12 '15 at 18:15
  • `[[SKPaymentQueue defaultQueue] addTransactionObserver:self];` is needed for `restore` method – AykutE Dec 23 '15 at 01:09
  • does the iTunes Connect US Tax form have to be filled out to test in app purchases? – Martin Mlostek Feb 03 '16 at 16:52
  • @Jojodmo where does the "purchase:" IBAction hook to? it's not clear in the tutorial! – George Asda Feb 07 '16 at 13:02
  • @GeorgeAsda It shouldn't be hooked up to anything, but it is confusing that `purchase:` is an IBAction, so I updated the post and removed the method from the .h file, and also changed it to a void in the .m file – Jojodmo Feb 07 '16 at 18:33
  • @Jojodmo I have two products and as per your comments I have defined another function. However when I hit restore then both purchases are available to the user even ONLY ONE has been purchased? – George Asda Feb 08 '16 at 11:29
  • @GeorgeAsda You must check which purchases were restored using `SKPaymentTransaction.payment.productIdentifier` to get the product identifiers - I have updated the post – Jojodmo Feb 13 '16 at 18:00
  • Oh thanks. Already did that. It was easy to figure it out myself. I used NSString *productID = transaction.payment.productIdentifier; [purchasedItemIDs addObject:productID]; // NSLog (@"product id is %@" , productID) if ([productID isEqualToString: kFirstInAppProductIdentifier]){ //restore first}. Many thanks for the great tutorial – George Asda Feb 13 '16 at 18:01
  • Can we edit the In-App purchase settings later in iTunes Connect? Thanks in advance. – Yuvaraj.M Mar 04 '16 at 05:27
  • @Yuvaraj.M Yes, you are able to add/edit languages, as well as change the price – Jojodmo Mar 05 '16 at 00:24
  • @Jojodmo Thank you :) – Yuvaraj.M Mar 05 '16 at 11:47
  • This is an amazing answer. I would note that as of iOS 8 you should support SKPaymentTransactionStateDeferred and move the SKPaymentTransactionObserver delegate methods to app delegate (or a singleton that is loaded on app load if you prefer not to clutter up the app delegate) in order to handle deferred payments. – Joel Jun 02 '16 at 06:26
  • Also note that this code won't work for auto renew subscription. StoreKit adds transactions to the queue automatically at each renewal so if you aren't observing the transaction queue globally you will miss the renewals. – Joel Jun 06 '16 at 20:42
  • If you don't have any swift files in your project, you will not have the option that says "Swift Compiler - Code Generation". Just create a dummy swift file and xcode will ask you to generate a bridging header. You can then skip step 3. – Emre Kenci Aug 17 '16 at 19:20
  • 1
    For those who found no products running this example. Add your contact, tax and everything until Apple lets you download the Agreement on "Agreements, Tax, and Banking" page. It would take awhile after your inputs. After that products will be found. – deathemperor Sep 12 '16 at 17:50
  • @Jojodmo your answer is really helpful, what if I have 2 product id (in-app purchase) in one view controller? how to handle this? – Nij Jan 12 '17 at 11:28
  • 1
    @Nij I'm really sorry for the late reply — if you haven't already figured it out, you need to create your two in-app purchases, then make another function with the same contents of `tapsRemoveAds` in the example, but use the other product ID. Then, in the `paymentQueue:(SKPaymentQueue *)queue updatedTransactions:` function, under the `SKPaymentTransactionStatePurchased ` part of the switch statement, check which product was purchased by checking `transaction.payment.productIdentifier` against the product IDs that you know. – Jojodmo Jan 20 '17 at 02:31
  • @Jojodmo this is exactly what I did. Thanks :) – Nij Jan 20 '17 at 05:40
  • @Joel Been pretty stumped with auto-renewals and can't seem to find something in Objective-C. Any suggestions? – SimplyLearning Feb 03 '17 at 08:51
  • Hi @Jojodmo I have implemented exactly what was mentioned in the steps but found that i am getting Invalid product id. Should we set up the Banking Information, Tax Info and Contact Info in Agreements, Tax and Banking? Please help me. – Pradeep Reddy Kypa Apr 03 '17 at 15:35
  • @PradeepReddyKypa Yes, you should set those up, and also make sure to wait about 24 hours from when you first created the product for the iTunes servers to sync. It's obvious, but also double-check that the product ID is spelled right, and is exactly what's on iTunes Connect (it's case-sensitive) – Jojodmo Apr 03 '17 at 16:20
  • Thanks @Jojodmo. I made sure that i have the same Product identifier entered. But i am yet to enter all the banking details and the agreement is still not under Contracts In Effect. – Pradeep Reddy Kypa Apr 03 '17 at 17:57
  • Thanks @jojodmo, this is the best answer I've never read on stack overflow!! Thank you again – Ragazzetto Apr 22 '17 at 04:58
  • How to use IAP with Touch ID? – Artem Zaytsev May 19 '17 at 10:02
  • @Jojodmo I have set up everything for the consumable inapp purchase in my app. Everything works fine on both development and adhoc profile but not working on the released app downloaded from the appstore. Product are not listed on the appstore version. Could not find the problem and solution as well. Please suggest me any idea. Thank You! – Purushottam Padhya Aug 15 '18 at 07:08
  • @PurushottamPadhya Have you submitted the IAP for review on iTunesConnect/App Store Connect, and has it been cleared for sale by Apple? As of now, that's the only thing I could think of that would cause it to not work on the App Store. – Jojodmo Aug 18 '18 at 23:54
  • do i need to upload binary to apple store connect first to test the IAP?since i keeping get No Products Available – showmyroutes Aug 25 '18 at 12:55
  • "Users and Roles" has been replaced by "Users and Access" – Pablo Alfonso Apr 06 '19 at 17:42
  • 1
    Someone send the author some cash. I looked far and wide for a reasonable intro to this topic and finally found this answer. What I love is that the author assumes you already know how to create UI elements, etc. Just the basics facts were exactly what I needed! – VTPete Nov 19 '20 at 14:03
14

Just translate Jojodmo code to Swift:

class InAppPurchaseManager: NSObject , SKProductsRequestDelegate, SKPaymentTransactionObserver{





//If you have more than one in-app purchase, you can define both of
//of them here. So, for example, you could define both kRemoveAdsProductIdentifier
//and kBuyCurrencyProductIdentifier with their respective product ids
//
//for this example, we will only use one product

let kRemoveAdsProductIdentifier = "put your product id (the one that we just made in iTunesConnect) in here"

@IBAction func tapsRemoveAds() {

    NSLog("User requests to remove ads")

    if SKPaymentQueue.canMakePayments() {
        NSLog("User can make payments")

        //If you have more than one in-app purchase, and would like
        //to have the user purchase a different product, simply define
        //another function and replace kRemoveAdsProductIdentifier with
        //the identifier for the other product
        let set : Set<String> = [kRemoveAdsProductIdentifier]
        let productsRequest = SKProductsRequest(productIdentifiers: set)
        productsRequest.delegate = self
        productsRequest.start()

    }
    else {
        NSLog("User cannot make payments due to parental controls")
        //this is called the user cannot make payments, most likely due to parental controls
    }
}


func purchase(product : SKProduct) {

    let payment = SKPayment(product: product)
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    SKPaymentQueue.defaultQueue().addPayment(payment)
}

func restore() {
    //this is called when the user restores purchases, you should hook this up to a button
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}


func doRemoveAds() {
    //TODO: implement
}

/////////////////////////////////////////////////
//////////////// store delegate /////////////////
/////////////////////////////////////////////////
// MARK: - store delegate -


func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {

    if let validProduct = response.products.first {
        NSLog("Products Available!")
        self.purchase(validProduct)
    }
    else {
        NSLog("No products available")
        //this is called if your product id is not valid, this shouldn't be called unless that happens.
    }
}

func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {


    NSLog("received restored transactions: \(queue.transactions.count)")
    for transaction in queue.transactions {
        if transaction.transactionState == .Restored {
            //called when the user successfully restores a purchase
            NSLog("Transaction state -> Restored")

            //if you have more than one in-app purchase product,
            //you restore the correct product for the identifier.
            //For example, you could use
            //if(productID == kRemoveAdsProductIdentifier)
            //to get the product identifier for the
            //restored purchases, you can use
            //
            //NSString *productID = transaction.payment.productIdentifier;
            self.doRemoveAds()
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
            break;
        }
    }
}


func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {

    for transaction in transactions {
        switch transaction.transactionState {
        case .Purchasing: NSLog("Transaction state -> Purchasing")
            //called when the user is in the process of purchasing, do not add any of your own code here.
        case .Purchased:
            //this is called when the user has successfully purchased the package (Cha-Ching!)
            self.doRemoveAds() //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
            NSLog("Transaction state -> Purchased")
        case .Restored:
            NSLog("Transaction state -> Restored")
            //add the same code as you did from SKPaymentTransactionStatePurchased here
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
        case .Failed:
            //called when the transaction does not finish
            if transaction.error?.code == SKErrorPaymentCancelled {
                NSLog("Transaction state -> Cancelled")
                //the user cancelled the payment ;(
            }
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
        case .Deferred:
            // The transaction is in the queue, but its final status is pending external action.
            NSLog("Transaction state -> Deferred")

        }


    }
}
} 
Yedidya Reiss
  • 5,307
  • 2
  • 15
  • 19
9

Swift Answer

This is meant to supplement my Objective-C answer for Swift users, to keep the Objective-C answer from getting too big.

Setup

First, set up the in-app purchase on appstoreconnect.apple.com. Follow the beginning part of my Objective-C answer (steps 1-13, under the App Store Connect header) for instructions on doing that.

It could take a few hours for your product ID to register in App Store Connect, so be patient.

Now that you've set up your in-app purchase information on App Store Connect, we need to add Apple's framework for in-app-purchases, StoreKit, to the app.

Go into your Xcode project, and go to the application manager (blue page-like icon at the top of the left bar where your app's files are). Click on your app under targets on the left (it should be the first option), then go to "Capabilities" at the top. On the list, you should see an option "In-App Purchase". Turn this capability ON, and Xcode will add StoreKit to your project.

Coding

Now, we're going to start coding!

First, make a new swift file that will manage all of your in-app-purchases. I'm going to call it IAPManager.swift.

In this file, we're going to create a new class, called IAPManager that is a SKProductsRequestDelegate and SKPaymentTransactionObserver. At the top, make sure you import Foundation and StoreKit

import Foundation
import StoreKit

public class IAPManager: NSObject, SKProductsRequestDelegate,
                         SKPaymentTransactionObserver {
}

Next, we're going to add a variable to define the identifier for our in-app purchase (you could also use an enum, which would be easier to maintain if you have multiple IAPs).

// This should the ID of the in-app-purchase you made on AppStore Connect.
// if you have multiple IAPs, you'll need to store their identifiers in
// other variables, too (or, preferably in an enum).
let removeAdsID = "com.skiplit.removeAds"

Let's add an initializer for our class next:

// This is the initializer for your IAPManager class
//
// A better, and more scaleable way of doing this
// is to also accept a callback in the initializer, and call
// that callback in places like the paymentQueue function, and
// in all functions in this class, in place of calls to functions
// in RemoveAdsManager (you'll see those calls in the code below).

let productID: String
init(productID: String){
    self.productID = productID
}

Now, we're going to add the required functions for SKProductsRequestDelegate and SKPaymentTransactionObserver to work:

We'll add the RemoveAdsManager class later

// This is called when a SKProductsRequest receives a response
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse){
    // Let's try to get the first product from the response
    // to the request
    if let product = response.products.first{
        // We were able to get the product! Make a new payment
        // using this product
        let payment = SKPayment(product: product)

        // add the new payment to the queue
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().add(payment)
    }
    else{
        // Something went wrong! It is likely that either
        // the user doesn't have internet connection, or
        // your product ID is wrong!
        //
        // Tell the user in requestFailed() by sending an alert,
        // or something of the sort

        RemoveAdsManager.removeAdsFailure()
    }
}

// This is called when the user restores their IAP sucessfully
private func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue){
    // For every transaction in the transaction queue...
    for transaction in queue.transactions{
        // If that transaction was restored
        if transaction.transactionState == .restored{
            // get the producted ID from the transaction
            let productID = transaction.payment.productIdentifier

            // In this case, we have only one IAP, so we don't need to check
            // what IAP it is. However, this is useful if you have multiple IAPs!
            // You'll need to figure out which one was restored
            if(productID.lowercased() == IAPManager.removeAdsID.lowercased()){
                // Restore the user's purchases
                RemoveAdsManager.restoreRemoveAdsSuccess()
            }

            // finish the payment
            SKPaymentQueue.default().finishTransaction(transaction)
        }
    }
}

// This is called when the state of the IAP changes -- from purchasing to purchased, for example.
// This is where the magic happens :)
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){
    for transaction in transactions{
        // get the producted ID from the transaction
        let productID = transaction.payment.productIdentifier

        // In this case, we have only one IAP, so we don't need to check
        // what IAP it is.
        // However, if you have multiple IAPs, you'll need to use productID
        // to check what functions you should run here!

        switch transaction.transactionState{
        case .purchasing:
            // if the user is currently purchasing the IAP,
            // we don't need to do anything.
            //
            // You could use this to show the user
            // an activity indicator, or something like that
            break
        case .purchased:
            // the user successfully purchased the IAP!
            RemoveAdsManager.removeAdsSuccess()
            SKPaymentQueue.default().finishTransaction(transaction)
        case .restored:
                // the user restored their IAP!
                IAPTestingHandler.restoreRemoveAdsSuccess()
                SKPaymentQueue.default().finishTransaction(transaction)
        case .failed:
                // The transaction failed!
                RemoveAdsManager.removeAdsFailure()
                // finish the transaction
                SKPaymentQueue.default().finishTransaction(transaction)
        case .deferred:
                // This happens when the IAP needs an external action
                // in order to proceeded, like Ask to Buy
                RemoveAdsManager.removeAdsDeferred()
                break
        }
    }
}

Now let's add some functions that can be used to start a purchase or a restore purchases:

// Call this when you want to begin a purchase
// for the productID you gave to the initializer
public func beginPurchase(){
    // If the user can make payments
    if SKPaymentQueue.canMakePayments(){
        // Create a new request
        let request = SKProductsRequest(productIdentifiers: [productID])
        // Set the request delegate to self, so we receive a response
        request.delegate = self
        // start the request
        request.start()
    }
    else{
        // Otherwise, tell the user that
        // they are not authorized to make payments,
        // due to parental controls, etc
    }
}

// Call this when you want to restore all purchases
// regardless of the productID you gave to the initializer
public func beginRestorePurchases(){
    // restore purchases, and give responses to self
    SKPaymentQueue.default().add(self)
    SKPaymentQueue.default().restoreCompletedTransactions()
}

Next, let's add a new utilities class to manage our IAPs. All of this code could be in one class, but having it multiple makes it a little cleaner. I'm going to make a new class called RemoveAdsManager, and in it, put a few functions

public class RemoveAdsManager{

    class func removeAds()
    class func restoreRemoveAds()

    class func areAdsRemoved() -> Bool

    class func removeAdsSuccess()
    class func restoreRemoveAdsSuccess()
    class func removeAdsDeferred()
    class func removeAdsFailure()
}

The first three functions, removeAds, restoreRemoveAds, and areAdsRemoved, are functions that you'll call to do certain actions. The last four are one that will be called by IAPManager.

Let's add some code to the first two functions, removeAds and restoreRemoveAds:

// Call this when the user wants
// to remove ads, like when they
// press a "remove ads" button
class func removeAds(){
    // Before starting the purchase, you could tell the
    // user that their purchase is happening, maybe with
    // an activity indicator

    let iap = IAPManager(productID: IAPManager.removeAdsID)
    iap.beginPurchase()
}

// Call this when the user wants
// to restore their IAP purchases,
// like when they press a "restore
// purchases" button.
class func restoreRemoveAds(){
    // Before starting the purchase, you could tell the
    // user that the restore action is happening, maybe with
    // an activity indicator

    let iap = IAPManager(productID: IAPManager.removeAdsID)
    iap.beginRestorePurchases()
}

And lastly, let's add some code to the last five functions.

// Call this to check whether or not
// ads are removed. You can use the
// result of this to hide or show
// ads
class func areAdsRemoved() -> Bool{
    // This is the code that is run to check
    // if the user has the IAP.

    return UserDefaults.standard.bool(forKey: "RemoveAdsPurchased")
}

// This will be called by IAPManager
// when the user sucessfully purchases
// the IAP
class func removeAdsSuccess(){
    // This is the code that is run to actually
    // give the IAP to the user!
    //
    // I'm using UserDefaults in this example,
    // but you may want to use Keychain,
    // or some other method, as UserDefaults
    // can be modified by users using their
    // computer, if they know how to, more
    // easily than Keychain

    UserDefaults.standard.set(true, forKey: "RemoveAdsPurchased")
    UserDefaults.standard.synchronize()
}

// This will be called by IAPManager
// when the user sucessfully restores
//  their purchases
class func restoreRemoveAdsSuccess(){
    // Give the user their IAP back! Likely all you'll need to
    // do is call the same function you call when a user
    // sucessfully completes their purchase. In this case, removeAdsSuccess()

    removeAdsSuccess()
}

// This will be called by IAPManager
// when the IAP failed
class func removeAdsFailure(){
    // Send the user a message explaining that the IAP
    // failed for some reason, and to try again later
}

// This will be called by IAPManager
// when the IAP gets deferred.
class func removeAdsDeferred(){
    // Send the user a message explaining that the IAP
    // was deferred, and pending an external action, like
    // Ask to Buy.
}

Putting it all together, we get something like this:

import Foundation
import StoreKit

public class RemoveAdsManager{

    // Call this when the user wants
    // to remove ads, like when they
    // press a "remove ads" button
    class func removeAds(){
        // Before starting the purchase, you could tell the
        // user that their purchase is happening, maybe with
        // an activity indicator

        let iap = IAPManager(productID: IAPManager.removeAdsID)
        iap.beginPurchase()
    }

    // Call this when the user wants
    // to restore their IAP purchases,
    // like when they press a "restore
    // purchases" button.
    class func restoreRemoveAds(){
        // Before starting the purchase, you could tell the
        // user that the restore action is happening, maybe with
        // an activity indicator

        let iap = IAPManager(productID: IAPManager.removeAdsID)
        iap.beginRestorePurchases()
    }

    // Call this to check whether or not
    // ads are removed. You can use the
    // result of this to hide or show
    // ads
    class func areAdsRemoved() -> Bool{
        // This is the code that is run to check
        // if the user has the IAP.

        return UserDefaults.standard.bool(forKey: "RemoveAdsPurchased")
    }

    // This will be called by IAPManager
    // when the user sucessfully purchases
    // the IAP
    class func removeAdsSuccess(){
        // This is the code that is run to actually
        // give the IAP to the user!
        //
        // I'm using UserDefaults in this example,
        // but you may want to use Keychain,
        // or some other method, as UserDefaults
        // can be modified by users using their
        // computer, if they know how to, more
        // easily than Keychain

        UserDefaults.standard.set(true, forKey: "RemoveAdsPurchased")
        UserDefaults.standard.synchronize()
    }

    // This will be called by IAPManager
    // when the user sucessfully restores
    //  their purchases
    class func restoreRemoveAdsSuccess(){
        // Give the user their IAP back! Likely all you'll need to
        // do is call the same function you call when a user
        // sucessfully completes their purchase. In this case, removeAdsSuccess()
        removeAdsSuccess()
    }

    // This will be called by IAPManager
    // when the IAP failed
    class func removeAdsFailure(){
        // Send the user a message explaining that the IAP
        // failed for some reason, and to try again later
    }

    // This will be called by IAPManager
    // when the IAP gets deferred.
    class func removeAdsDeferred(){
        // Send the user a message explaining that the IAP
        // was deferred, and pending an external action, like
        // Ask to Buy.
    }

}

public class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver{

    // This should the ID of the in-app-purchase you made on AppStore Connect.
    // if you have multiple IAPs, you'll need to store their identifiers in
    // other variables, too (or, preferably in an enum).
    static let removeAdsID = "com.skiplit.removeAds"

    // This is the initializer for your IAPManager class
    //
    // An alternative, and more scaleable way of doing this
    // is to also accept a callback in the initializer, and call
    // that callback in places like the paymentQueue function, and
    // in all functions in this class, in place of calls to functions
    // in RemoveAdsManager.
    let productID: String
    init(productID: String){
        self.productID = productID
    }

    // Call this when you want to begin a purchase
    // for the productID you gave to the initializer
    public func beginPurchase(){
        // If the user can make payments
        if SKPaymentQueue.canMakePayments(){
            // Create a new request
            let request = SKProductsRequest(productIdentifiers: [productID])
            request.delegate = self
            request.start()
        }
        else{
            // Otherwise, tell the user that
            // they are not authorized to make payments,
            // due to parental controls, etc
        }
    }

    // Call this when you want to restore all purchases
    // regardless of the productID you gave to the initializer
    public func beginRestorePurchases(){
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

    // This is called when a SKProductsRequest receives a response
    public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse){
        // Let's try to get the first product from the response
        // to the request
        if let product = response.products.first{
            // We were able to get the product! Make a new payment
            // using this product
            let payment = SKPayment(product: product)

            // add the new payment to the queue
            SKPaymentQueue.default().add(self)
            SKPaymentQueue.default().add(payment)
        }
        else{
            // Something went wrong! It is likely that either
            // the user doesn't have internet connection, or
            // your product ID is wrong!
            //
            // Tell the user in requestFailed() by sending an alert,
            // or something of the sort

            RemoveAdsManager.removeAdsFailure()
        }
    }

    // This is called when the user restores their IAP sucessfully
    private func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue){
        // For every transaction in the transaction queue...
        for transaction in queue.transactions{
            // If that transaction was restored
            if transaction.transactionState == .restored{
                // get the producted ID from the transaction
                let productID = transaction.payment.productIdentifier

                // In this case, we have only one IAP, so we don't need to check
                // what IAP it is. However, this is useful if you have multiple IAPs!
                // You'll need to figure out which one was restored
                if(productID.lowercased() == IAPManager.removeAdsID.lowercased()){
                    // Restore the user's purchases
                    RemoveAdsManager.restoreRemoveAdsSuccess()
                }

                // finish the payment
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }
    }

    // This is called when the state of the IAP changes -- from purchasing to purchased, for example.
    // This is where the magic happens :)
    public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){
        for transaction in transactions{
            // get the producted ID from the transaction
            let productID = transaction.payment.productIdentifier

            // In this case, we have only one IAP, so we don't need to check
            // what IAP it is.
            // However, if you have multiple IAPs, you'll need to use productID
            // to check what functions you should run here!

            switch transaction.transactionState{
            case .purchasing:
                // if the user is currently purchasing the IAP,
                // we don't need to do anything.
                //
                // You could use this to show the user
                // an activity indicator, or something like that
                break
            case .purchased:
                // the user sucessfully purchased the IAP!
                RemoveAdsManager.removeAdsSuccess()
                SKPaymentQueue.default().finishTransaction(transaction)
            case .restored:
                // the user restored their IAP!
                RemoveAdsManager.restoreRemoveAdsSuccess()
                SKPaymentQueue.default().finishTransaction(transaction)
            case .failed:
                // The transaction failed!
                RemoveAdsManager.removeAdsFailure()
                // finish the transaction
                SKPaymentQueue.default().finishTransaction(transaction)
            case .deferred:
                // This happens when the IAP needs an external action
                // in order to proceeded, like Ask to Buy
                RemoveAdsManager.removeAdsDeferred()
                break
            }
        }
    }

}

Lastly, you need to add some way for the user to start the purchase and call RemoveAdsManager.removeAds() and start a restore and call RemoveAdsManager.restoreRemoveAds(), like a button somewhere! Keep in mind that, per the App Store guidelines, you do need to provide a button to restore purchases somewhere.

Submitting for review

The last thing to do is submit your IAP for review on App Store Connect! For detailed instructions on doing that, you can follow the last part of my Objective-C answer, under the Submitting for review header.

Jojodmo
  • 22,792
  • 13
  • 59
  • 99
4

RMStore is a lightweight iOS library for In-App Purchases. It wraps StoreKit API and provides you with handy blocks for asynchronous requests. Purchasing a product is as easy as calling a single method.

For the advanced users, this library also provides receipt verification, content downloads and transaction persistence.

Vladimir Grigorov
  • 9,951
  • 7
  • 55
  • 65
-1

I know I am quite late to post this, but I share similar experience when I learned the ropes of IAP model.

In-app purchase is one of the most comprehensive workflow in iOS implemented by Storekit framework. The entire documentation is quite clear if you patience to read it, but is somewhat advanced in nature of technicality.

To summarize:

1 - Request the products - use SKProductRequest & SKProductRequestDelegate classes to issue request for Product IDs and receive them back from your own itunesconnect store.

These SKProducts should be used to populate your store UI which the user can use to buy a specific product.

2 - Issue payment request - use SKPayment & SKPaymentQueue to add payment to the transaction queue.

3 - Monitor transaction queue for status update - use SKPaymentTransactionObserver Protocol's updatedTransactions method to monitor status:

SKPaymentTransactionStatePurchasing - don't do anything
SKPaymentTransactionStatePurchased - unlock product, finish the transaction
SKPaymentTransactionStateFailed - show error, finish the transaction
SKPaymentTransactionStateRestored - unlock product, finish the transaction

4 - Restore button flow - use SKPaymentQueue's restoreCompletedTransactions to accomplish this - step 3 will take care of the rest, along with SKPaymentTransactionObserver's following methods:

paymentQueueRestoreCompletedTransactionsFinished
restoreCompletedTransactionsFailedWithError

Here is a step by step tutorial (authored by me as a result of my own attempts to understand it) that explains it. At the end it also provides code sample that you can directly use.

Here is another one I created to explain certain things that only text could describe in better manner.

Nirav Bhatt
  • 6,682
  • 5
  • 39
  • 82
  • 21
    StackOverflow is a website for helping others, and is not for trying to make money off them. You should either remove the second to last link, or just post what is done in that tutorial here, for free. – Jojodmo Apr 16 '15 at 04:29
  • @Jojodmo can you substantiate your claim with any guidelines by SO? I see a lot of people marketing their own SDK (even paid one) with a disclaimer, which I think is very much present here too. – Nirav Bhatt Apr 21 '15 at 14:29
  • 12
    There are no guidelines against it, but if you're here to make money, you're probably here for the wrong reasons. IMO, your answer seems to be focused on getting people to enroll in your video tutorials, and not on helping others – Jojodmo Apr 21 '15 at 16:48
  • 3
    This is just nothing but annoyance. – durazno Nov 17 '15 at 15:34