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:
- 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.
- Parse the in-app purchase receipts. Each in-app purchase receipt consists of a set of attributes, like the application’s receipt does.
- 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
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
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.
You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).
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.
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”.
Next if you don’t already have any listed, click “Create New”.
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.
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.
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.
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.
I’ve created an In-App Purchase for each one of the 5 types for testing.
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.
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.
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.
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.
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).
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.
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).
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.
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.