Brian Coleman

  • Home
  • ABOUT
  • SERVICES
  • PORTFOLIO
  • BLOG
  • Contact

Iap 2

Tutorial: Receipt Validation in Swift

After you have processed your In-App Purchases you may want to verify the receipt with the App Store directly. You’ll get more details about the purchase so you can store that information in your database. There are two ways: first connecting to the Apple App Store directly within your app, and second send the receipt data to your own server and having your server perform the validation with the App Store server. The second is the easiest and most secure method when verifying receipts since it protects against Man-in-the-middle (MITM) attacks where its possible to get free in-app purchases in any app.

Apple recommends you use a trusted server to communicate with the App Store. Using your own server lets you design your app to recognize and trust only your server, and lets you ensure that your server connects with the App Store server. It is not possible to build a trusted connection between a user’s device and the App Store directly because you don’t control either end of that connection.

Validate In-App Purchases

To validate an in-app purchase, your application performs the following tests, in order:

  1. Parse and validate the application’s receipt, as described in the previous sections. If the receipt is not valid, none of the in-app purchases are valid.
  2. Parse the in-app purchase receipts. Each in-app purchase receipt consists of a set of attributes, like the application’s receipt does.
  3. Examine the product identifier for each in-app purchase receipt and enable the corresponding functionality or content in your app.

If validation of an in-app purchase receipt fails, your application simply does not enable the functionality or content.

To begin, be sure you already have In-App Purchases implement in your app. You can follow the steps in this article to integrate In-App Purchases: Tutorial: In-App Purchases (IAP) in Swift.

Send Receipt Data to your Server

Use the validateReceipt() method below to retrieve the receipt data, then send this data to your server. Submit the receipt object as the payload of an HTTP POST request, and encode the data using base64 encoding. Make sure you change the request URL to point to the PHP script on your server. Next, if the PHP script communicates correctly with Apple’s server you will need to parse the response with all of the receipt and transaction information.

func validateReceipt() {
    var response: NSURLResponse?
    var error: NSError?
    
    var receiptUrl = NSBundle.mainBundle().appStoreReceiptURL
    var receipt: NSData = NSData(contentsOfURL:receiptUrl!, options: nil, error: nil)!
    
    var receiptdata: NSString = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
    //println(receiptdata)
    
    var request = NSMutableURLRequest(URL: NSURL(string: "http://www.brianjcoleman.com/code/verifyReceipt.php")!)
    
    var session = NSURLSession.sharedSession()
    request.HTTPMethod = "POST"
    
    var err: NSError?
    
    request.HTTPBody = receiptdata.dataUsingEncoding(NSASCIIStringEncoding)
    
    var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
        var err: NSError?
        var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary
        
        if(err != nil)
        {
            println(err!.localizedDescription)
            let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
            println("Error could not parse JSON: '\(jsonStr)'")
        }
        else {
            if let parseJSON = json {
                println("Receipt \(parseJSON)")
            }
            else {
                let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
                println("Receipt Error: \(jsonStr)")
            }
        }
    })
    
    task.resume()
}

In-App Purchase Shared Secret

If your application provides Auto-Renewable Subscriptions you will need to send a password along with the receipt data to Apple during validation. If you’re validating a receipt for an auto-renewing receipt, go to iTunes Connect and get the hexadecimal shared secret for your app.

The secret is typically a 32 digit, alpha-numeric string that looks something like this e4b2dd30b7rt4a7382b5173hg790elk7 (this one is fake).

In iTunes Connect go to My Apps > (select your app) > In-App Purchases > View or generate a shared secret

Screen Shot 2015-05-24 at 8.46.05 PM2

Send Receipt Data to Apple using PHP

Use the following PHP script to take the receipt data and send it to Apple’s server to validate. The script takes two values, the receipt-data and a password (if needed for auto-renewable subscriptions). You must replace this text with the secret key that you get from iTunes Connect. If you don’t have an auto-renewable subscription, you can remove the password parameter and only send the receipt-data.

Save this file as “verifyReceipt.php”, upload it to your web server and change the permissions to 755.

$value){
	$newcontent .= $key.' '.$value;
}

$new = trim($newcontent);
$new = trim($newcontent);
$new = str_replace('_','+',$new);
$new = str_replace(' =','==',$new);

if (substr_count($new,'=') == 0){
if (strpos('=',$new) === false){
		$new .= '=';
}
}

$new = '{"receipt-data":"'.$new.'","password":""}';
$info = getReceiptData($new);
	?>

Testing

To test the receipt validation, run your application and buy one of the in-app purchases, call the validateReceipt() method to initiate the receipt-data to send to your server where the PHP will pass the receipt (and possible the shared secret password), then get the response back from Apple and send it back to the iOS app where you can parse the JSON and get the receipt information.

During testing you may see some status codes returned from Apple, here is a table that describes each of them:

Status Code Description
21000 The App Store could not read the JSON object you provided.
21002 The data in the receipt-data property was malformed or missing.
21003 The receipt could not be authenticated.
21004 The shared secret you provided does not match the shared secret on file for your account.
Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
21005 The receipt server is not currently available.
21006 This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
21007 This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.
21008 This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.

Receipt Data

Below is a sample of the data you will receive for the receipt. You may want to parse this JSON and store the values into a database so you can track your users status.

 "latest_receipt_info" =     (
             {
         "is_trial_period" = false;
         "original_purchase_date" = "2015-05-20 17:41:12 Etc/GMT";
         "original_purchase_date_ms" = 1432143672000;
         "original_purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles";
         "original_transaction_id" = 1000000156014803;
         "product_id" = "com.brianjcoleman.testiap1";
         "purchase_date" = "2015-05-20 17:41:12 Etc/GMT";
         "purchase_date_ms" = 1432143672000;
         "purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles";
         quantity = 1;
         "transaction_id" = 1000000156014803;
     },
             {
         "expires_date" = "2015-05-23 17:05:59 Etc/GMT";
         "expires_date_ms" = 1432400759000;
         "expires_date_pst" = "2015-05-23 10:05:59 America/Los_Angeles";
         "is_trial_period" = false;
         "original_purchase_date" = "2015-05-23 17:03:00 Etc/GMT";
         "original_purchase_date_ms" = 1432400580000;
         "original_purchase_date_pst" = "2015-05-23 10:03:00 America/Los_Angeles";
         "original_transaction_id" = 1000000156451343;
         "product_id" = "com.brianjcoleman.testiap3";
         "purchase_date" = "2015-05-23 17:02:59 Etc/GMT";
         "purchase_date_ms" = 1432400579000;
         "purchase_date_pst" = "2015-05-23 10:02:59 America/Los_Angeles";
         quantity = 1;
         "transaction_id" = 1000000156451343;
         "web_order_line_item_id" = 1000000029801713;
     }
 );
 receipt =     {
     "adam_id" = 0;
     "app_item_id" = 0;
     "application_version" = 1;
     "bundle_id" = "com.brianjcoleman.iqtest";
     "download_id" = 0;
     "in_app" =         (
                     {
             "is_trial_period" = false;
             "original_purchase_date" = "2015-05-24 01:06:58 Etc/GMT";
             "original_purchase_date_ms" = 1432429618000;
             "original_purchase_date_pst" = "2015-05-23 18:06:58 America/Los_Angeles";
             "original_transaction_id" = 1000000156455961;
             "product_id" = "com.brianjcoleman.testiap2";
             "purchase_date" = "2015-05-24 01:06:58 Etc/GMT";
             "purchase_date_ms" = 1432429618000;
             "purchase_date_pst" = "2015-05-23 18:06:58 America/Los_Angeles";
             quantity = 1;
             "transaction_id" = 1000000156455961;
         },
                     {
             "is_trial_period" = false;
             "original_purchase_date" = "2015-05-20 17:41:12 Etc/GMT";
             "original_purchase_date_ms" = 1432143672000;
             "original_purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles";
             "original_transaction_id" = 1000000156014803;
             "product_id" = "com.brianjcoleman.testiap1";
             "purchase_date" = "2015-05-20 17:41:12 Etc/GMT";
             "purchase_date_ms" = 1432143672000;
             "purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles";
             quantity = 1;
             "transaction_id" = 1000000156014803;
         }
     );
     "original_application_version" = "1.0";
     "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
     "original_purchase_date_ms" = 1375340400000;
     "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
     "receipt_type" = ProductionSandbox;
     "request_date" = "2015-05-24 16:31:18 Etc/GMT";
     "request_date_ms" = 1432485078143;
     "request_date_pst" = "2015-05-24 09:31:18 America/Los_Angeles";
     "version_external_identifier" = 0;
 };
 status = 0;
}

You can learn more about the Receipt Fields in the Apple Validation Programming Guide.

Note that while testing auto-renewable subscriptions the “expires_date” in the sandbox will not act like it does in production. When testing auto-renewable subscriptions in the test environment, the duration times are compressed. Additionally, test subscriptions only auto-renew a maximum of six times.

Screen Shot 2015-05-26 at 8.42.24 AM

You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).

May 25, 2015 Swift, Tutorialsiap, ios8, swift, tutorial

Tutorial: In-App Purchases in Swift

In this tutorial we are going to review how to setup an In-App Purchase (IAP) for your app using Swift. This will show you step by step how to setup an IAP in iTunes Connect for your app, then how to implement it in your app and how you can unlock features or consumables for the user.

Enable IAP in the Developer Portal

The first step is to enable In-App Purchases when you create the App ID for your app. Make sure this is correct by logging into the developer center, then navigating to Certificates, Identifiers & Profiles, App Ids and checking that In-App Purchases are Enabled. They are enabled by default when you create a new app.

Screen Shot 2015-05-19 at 9.58.47 AM

Define the IAP in iTunes Connect

The next step is to list the In-App Purchases you would like to include in your app. To do this you already need to have your bundle ID and your app setup in iTunes Connect. To begin listing your IAP’s, go into your app within iTunes Connect and select “In-App Purchases”.

Screen Shot 2015-05-19 at 9.38.43 AM

Next if you don’t already have any listed, click “Create New”.

Screen Shot 2015-05-19 at 9.38.52 AM

Then you’ll need to select the type of In-App Purchase you need from the 5 types. There are two major types, Consumables and Subscriptions. Both are divided into two categories: for Consumables, we have Non-Consumable and Consumable. For Subscriptions, we have Renewable and Non-Renewable subscriptions. The choice of which one to implement will depend on your application and what you need to give users access to with their purchase.

Screen Shot 2015-05-19 at 9.39.25 AM

For each IAP that you define, you need to enter a Reference Name (only displayed to you to remember which IAP is which), Product ID (the string thats used in the code to reference this product) and Price Tier (How much you are going to charge). Every item must also have a product identifier that is associated with your application and uniquely identifies an item sold. Your application uses this product id to fetch localized item descriptions and pricing information from the App Store and to request payments.

Screen Shot 2015-05-19 at 1.20.05 PM

Enter in the localized item name and description per language that your app supports. This will be displayed to the user before they agree to pay for your product.

Screen Shot 2015-05-19 at 1.32.17 PM

Make sure you upload a screenshot for each IAP to show how it works, and where it is purchased from. You can also leave a review note so that you can explain the overall idea and flow. This will help the review team understand what the IAP is for and may save you a lot of back and forth and get it approved faster. The screenshots and the review note won’t be seen by the end users, they only support the app review team when they are reviewing your app.

Screen Shot 2015-05-19 at 1.34.37 PM

I’ve created an In-App Purchase for each one of the 5 types for testing.

Screen Shot 2015-05-19 at 1.41.36 PM

Implementing StoreKit

The interactions between the user, your app, and the App Store during the In-App Purchase process take place in three stages, as shown below. First, the user navigates to your app’s store and your app displays its products. Second, the user selects a product to buy and the app requests payment from the App Store. Third, the App Store processes the payment and your app delivers the purchased product.

intro_2x

First let’s start off by declaring the delegates and properties needed for the class. The SKProductsRequestDelegate is used for the callback method needed when retrieving the product information from iTunes Connect.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SKProductsRequestDelegate {

var tableView = UITableView()
let productIdentifiers = Set(["com.brianjcoleman.testiap1", "com.brianjcoleman.testiap2", "com.brianjcoleman.testiap3", "com.brianjcoleman.testiap4", "com.brianjcoleman.testiap5"])
var product: SKProduct?
var productsArray = Array()

We are going to use a UITableView to display our products. There is a set defined with the unique product identifiers that we setup in iTunes Connect for each one of our products. Finally the product is of type SKProduct which is an object that contains the title, description and price for a product. Then the productArray will be used to store all of the products we receive before we present it to the user.

Stage 1: Retrieving Product Information

To make sure your users see only products that are actually available for purchase, query the App Store before displaying your app’s store UI. This is valuable so you can change the title, description and prices for your products anytime without having to update your app.

In the viewDidLoad(), call the requestProductData() method to retrieve the product information for each one of the product identifiers defined above. It’s best to do a check to determine if the users device has In-App Purchases enabled. If they can make payments then the SKProductsRequest is made to iTunes Connect.

func requestProductData()
{
    if SKPaymentQueue.canMakePayments() {
        let request = SKProductsRequest(productIdentifiers:
            self.productIdentifiers as Set)
        request.delegate = self
        request.start()
    } else {
        var alert = UIAlertController(title: "In-App Purchases Not Enabled", message: "Please enable In App Purchase in Settings", preferredStyle: UIAlertControllerStyle.Alert)
        alert.addAction(UIAlertAction(title: "Settings", style: UIAlertActionStyle.Default, handler: { alertAction in
            alert.dismissViewControllerAnimated(true, completion: nil)
            
            let url: NSURL? = NSURL(string: UIApplicationOpenSettingsURLString)
            if url != nil
            {
                UIApplication.sharedApplication().openURL(url!)
            }
            
        }))
        alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: { alertAction in
            alert.dismissViewControllerAnimated(true, completion: nil)
        }))
        self.presentViewController(alert, animated: true, completion: nil)
    }
}

func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
    
    var products = response.products
    
    if (products.count != 0) {
        for var i = 0; i < products.count; i++
        {
            self.product = products[i] as? SKProduct
            self.productsArray.append(product!)
        }
        self.tableView.reloadData()
    } else {
        println("No products found")
    }
    
    products = response.invalidProductIdentifiers
    
    for product in products
    {
        println("Product not found: \(product)")
    }
}

Since the delegate SKProductsRequestDelegate is used, after the SKProductsRequest is made the callback method productsRequest is performed. In this method we store all of the product objects into the productsArray so we can populate our UITableView with the products so the user can select one to purchase.

Run your app to ensure the In-App Purchases are coming back from iTunes Connect. Here's what mine look like after I've added them to a UITableView.

Screen Shot 2015-05-22 at 4.18.32 PM

While in development it's good to check to see if all of your products are coming through correctly by adding the error checking for invalid product identifiers. If you are not getting any products back from Apple when you are trying to retrieve them look to this checklist:
http://www.gamedonia.com/game-development/solve-invalid-product-ids.

Stage 2: Requesting Payment

So now you've presented the list of products to the user, what's next? Well if you're providing good value or an amazing feature the user wants then they will want to buy one of your products.

func buyProduct(sender: UIButton) {
    let payment = SKPayment(product: productsArray[sender.tag])
    SKPaymentQueue.defaultQueue().addPayment(payment)
}

This method uses the button.tag property to get the correct product from the productsArray so the same method can be used for all products. You could also setup a separate function per product.

The user will then be prompted with the standard Apple In-App Purchase dialog asking them to confirm their purchase.

Screen Shot 2015-05-23 at 12.45.42 PM

Note: While you can test up to here using the Simulator you will need to test on the device to actually make a successful purchase. To test all In-App Purchases before they are submitted to Apple for review you'll be working in the Sandbox mode. Start by setting up a Sandbox (test account) in from iTunes Connect account. Users & Roles > Sandbox Testers.

Screen Shot 2015-05-23 at 12.45.02 PM

When testing on a device be sure to log out of your production iTunes account in the App Store before trying to purchase from a Sandbox account so you'll be prompted to login using your Sandbox Testing account. On the Simulator it will ask you to login (as shown below).

Screen Shot 2015-05-23 at 12.43.32 PM

Step 3 - Delivering Products

Once the user has confirmed they would like to purchase your product, the paymentQueue delegate method is invoked, but in order to be able to use the updatedTransactions method we need to declare it's delegate "SKPaymentTransactionObserver" in our class first.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver

Also add "SKPaymentQueue.defaultQueue().addTransactionObserver(self)" to your viewDidLoad to register your class with the delegate and add the transaction observer. Now we can add the following function to capture the approved or failed transaction from Apple.

func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
    
    for transaction in transactions as! [SKPaymentTransaction] {
        
        switch transaction.transactionState {
            
        case SKPaymentTransactionState.Purchased:
            println("Transaction Approved")
            println("Product Identifier: \(transaction.payment.productIdentifier)")
            self.deliverProduct(transaction)
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
            
        case SKPaymentTransactionState.Failed:
            println("Transaction Failed")
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
        default:
            break
        }
    }
}

func deliverProduct(transaction:SKPaymentTransaction) {
    
    if transaction.payment.productIdentifier == "com.brianjcoleman.testiap1"
    {
        println("Consumable Product Purchased")
        // Unlock Feature
    }
    else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap2"
    {
        println("Non-Consumable Product Purchased")
        // Unlock Feature
    }
    else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap3"
    {
        println("Auto-Renewable Subscription Product Purchased")
        // Unlock Feature
    }
    else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap4"
    {
        println("Free Subscription Product Purchased")
        // Unlock Feature
    }
    else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap5"
    {
        println("Non-Renewing Subscription Product Purchased")
        // Unlock Feature
    }
}

If the purchase is approved we need to make sure to deliver the functionality or product to the user and remove the transaction from the queue. In this example I am capturing the transaction and passing it to a deliverProduct method where I'll unlock the feature for the user.

IMG_0342

Restore Purchases

If your app offers Non-Consumable or any Subscription based In-App Purchases Apple requires that you provide a Restore Purchases button so users who have already purchased your product can get access to it again if they use your app on another Apple device (i.e. another iPhone, iPad or Apple TV).

IMG_03433

func restorePurchases(sender: UIButton) {
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}

func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
    println("Transactions Restored")
    
    var purchasedItemIDS = []
    for transaction:SKPaymentTransaction in queue.transactions as! [SKPaymentTransaction] {

        if transaction.payment.productIdentifier == "com.brianjcoleman.testiap1"
        {
            println("Consumable Product Purchased")
            // Unlock Feature
        }
        else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap2"
        {
            println("Non-Consumable Product Purchased")
            // Unlock Feature
        }
        else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap3"
        {
            println("Auto-Renewable Subscription Product Purchased")
            // Unlock Feature
        }
        else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap4"
        {
            println("Free Subscription Product Purchased")
            // Unlock Feature
        }
        else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap5"
        {
            println("Non-Renewing Subscription Product Purchased")
            // Unlock Feature
        }
    }
    
    var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK")
    alert.show()
}

The paymentQueueRestoreCompletedTransactionsFinished delegate method will loop through each In-App Purchase and check the product identifier of it. For each product identifier it find’s it will unlock the feature, just like the original deliverProduct(transaction:SKPaymentTransaction) method.

IMG_0344

You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).

If you would like to add receipt validation code to your app to prevent unauthorized purchases and keep a record of successful in-app purchases read Tutorial: Receipt Validation in Swift.

May 25, 2015 Swift, Tutorialsiap, ios8, swift, tutorial
Recent Posts
  • Classix for iPhone, iPad & Apple TV
  • Tutorial: How to test your app for IPv6 compatibility
  • Tutorial: Testing SSL using Charles Proxy on an iOS Device
  • Tutorial: 3D Touch – Quick Actions in Swift
  • tvOS Tutorial: Top Shelf in Swift
Featured Apps
Classix
Sportsnet
TAGS
tutorialswiftios8iosobjective-cvideogamesstrategynewsframeworkappsmonitizefacebookwatchappleios7toolstvosios9apiprovisionsocialtutorialsbooksdesignbookiapIPv6iTunes Connect
Search
TAGS
tutorialswiftios8iosobjective-cvideogamesstrategynewsframeworkappsmonitizefacebookwatchappleios7toolstvosios9apiprovisionsocialtutorialsbooksdesignbookiapIPv6iTunes Connect
ABOUT
Brian is a Lead iOS/tvOS Developer from Toronto with over 18 years of multifaceted experience including development, design, business analysis and project management.

FOLLOW ME
    
Email Subscription
Sign up for my newsletter to receive the latest news and tutorials posted.

Enter your email address:

2023 © Brian Coleman