Restoring non-consumable IAPs in iOS
You’d think that a Google search and subsequent visit to Stack Overflow are the solution to all your programming problems. Right? Until, I don’t find an answer or I find multiple contrasting answers. This adventure follows in the second category.
I needed to restore a non-consumable IAP (in-app purchase) for an iOS app. When you search for something like that you end up with a lot of noisy and contrasting information.I just needed to “discover” the freaking method to check if the user has already purchased a IAP that unlocks some features in the app I am working on. Sounds easy? Not so much.
After one hour of Googling, (one hour is a long time for me) I discovered two possibilities:
- one based on
SKPaymentQueue
’srestoreCompletedTransactions
- one based on
SKReceiptRefreshRequest
I oscillated between the two for half a day. No, I didn’t spend half a day coding, but testing IAPs is a huge pain in the neck. It has to be done on the device, you need to create sandbox users, don’t blow them by logging in the App Store, and enter their credentials many many times. Hammer on until you get it right.
At the beginning I was confident about the first approach, restoreCompletedTransactions
, because Apple’s documentation says
Your application calls this method to restore transactions that were previously finished so that you can process them again. For example, your application would use this to allow a user to unlock previously purchased content onto a new device.
Apple documentation source
Sounds perfect right? Except I spotted a few issues. The first is that is “freezes” the UI for quite a bit. If this method is meant to be run in the background, the doc should probably mention it. Anyway I don’t see why it shouldn’t be on the main thread. The pattern for restoring is pretty common:
- user taps restore
- show spinner
- validate
- hide spinner
- congratulate
- unlock features
Note: my IAP doesn’t need to download content after purchase/restore.
During the restore the user might be prompted to enter the iTunes connect password, totally legit action triggered by the operative system. If the user taps “Cancel”, for whatever reason, the UI again freezes and there’s no callback that allows you to show a “we couldn’t restore” kind of message.
So I dove into the second alternative, SKReceiptRefreshRequest
. The API is pretty simple at the beginning.
- create a request
- set a delegate
- implement delegate methods.
But that is just the first step of a long dance. The class above simply refreshes the receipt of the app. The receipt is stored in the bundle and might contain info about in-app purchases. But can you trust to load some content from the app bundle as evidence of a purchase? No! Because somebody can tamper the bundle and inject something that looks valid. So? You need to validate the receipt. How? There’s two ways:
- locally
- against Apple’s servers
Locally involves a very complicated procedure. Here’s Apple documentation about it. Here’s a post on objc.io that seems well done, but honestly I didn’t venture into it. I went with the second option.
So once you have loaded the receipt from the bundle you send it as a post request to Apple’s servers. Servers will reply with some JSON. You’ll parse the JSON to figure out if the user has already purchased a IAP in the past. Here’s how to load the receipt from the bundle.
if let rPath = NSBundle.mainBundle().appStoreReceiptURL?.path {
let res = NSFileManager.defaultManager().fileExistsAtPath(rPath)
if (res) {
validateReceipt()
} else {
notifyReceiptResult(false)
}
}
Here’s a horrible implementation of validateReceipt()
.
func validateReceipt() {
let recURL = NSBundle.mainBundle().appStoreReceiptURL!
let contents = NSData(contentsOfURL: recURL)
let receiptData = contents!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
let requestContents = ["receipt-data" : receiptData]
let requestData = try? NSJSONSerialization.dataWithJSONObject(requestContents, options: [])
let serverURL = "https://sandbox.itunes.apple.com/verifyReceipt" // TODO:change this in production with https://buy.itunes.apple.com/verifyReceipt
let url = NSURL(string: serverURL)
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
request.HTTPBody = requestData
let task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
guard let data = data where error == nil else {
self.notifyReceiptResult(false)
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options:[])
if let receipt = json["receipt"] as? [String: AnyObject],
let inApp = receipt["in_app"] as? [AnyObject] {
if (inApp.count > 0) {
self.notifyReceiptResult(true)
} else {
self.notifyReceiptResult(false)
}
}
}
catch let error as NSError {
print(error)
self.notifyReceiptResult(false)
}
})
task.resume()
}
It’s horrible because my goal was just making it work. At least it shows everything you need to know about the procedure. It’s the example I’d have liked to find somewhere on Google or Stack Overflow. With the fundamentals explained and ready to be modelled further.
notifyReceiptResult()
is a method that you can define to call a delegate, post an NSNotification
or notify who’s listening of the success/error of the restore procedure.
When I felt I was done I discovered that it’s not enough to check for the presence of a receipt. In fact, the receipt that you are validating is related to the purchase of the app itself. The receipt has a field in_app
that can contain the IAP receipts that you are looking for. In my case things are simple, that’s one IAP so I just check if the count of elements in in_app
is greater than 0 to declare success. Your mileage may vary.
It’s been a while since I went through a hunt like this. I am noticing more and more noise as time goes by. Apple introduces new APIs, deprecates old ones and finding what you are looking for takes more time. Yesterday I proposed a single method call to validate a IAP receipt. Let’s hope for 2017.
Note: I should point out that the disadvantage of validating a receipt against Apple’s servers is that the user has to be connected to the Internet to restore the purchase. The local method of validation doesn’t require a connection.
Special thanks to Kyle Gorlick that showed me some of his Objc code that put me on the right track.