I am developing an iOS app in Swift 3 and trying to implement receipt validation following this tutorial: http://savvyapps.com/blog/how-setup-test-auto-renewable-subscription-ios-app. However, the tutorial seems to have been written using an earlier version of Swift, so I had to make several changes. Here is my receiptValidation() function:
func receiptValidation() {
let receiptPath = Bundle.main.appStoreReceiptURL?.path
if FileManager.default.fileExists(atPath: receiptPath!){
var receiptData:NSData?
do{
receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
}
catch{
print("ERROR: " + error.localizedDescription)
}
let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let postString = "receipt-data=" + receiptString! + "&password=" + SUBSCRIPTION_SECRET
let storeURL = NSURL(string:"https://sandbox.itunes.apple.com/verifyReceipt")!
let storeRequest = NSMutableURLRequest(url: storeURL as URL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = postString.data(using: .utf8)
let session = URLSession(configuration:URLSessionConfiguration.default)
let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error in
do{
let jsonResponse:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
let expirationDate:NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse)!
self.updateIAPExpirationDate(date: expirationDate)
}
catch{
print("ERROR: " + error.localizedDescription)
}
}
task.resume()
}
}
The problem shows up when I try to call the expirationDateFromResponse() method. It turns out that the jsonResponse that gets passed to this method only contains: status = 21002;
. I looked this up and it means "The data in the receipt-data property was malformed or missing." However, the device I'm testing on has an active sandbox subscription for the product, and the subscription seems to work correctly aside from this issue. Is there something else I still need to do to make sure the receiptData value will be read and encoded correctly, or some other issue that might be causing this problem?
EDIT:
I tried an alternate way of setting storeRequest.httpBody:
func receiptValidation() {
let receiptPath = Bundle.main.appStoreReceiptURL?.path
if FileManager.default.fileExists(atPath: receiptPath!){
var receiptData:NSData?
do{
receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
}
catch{
print("ERROR: " + error.localizedDescription)
}
let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) //.URLEncoded
let dict = ["receipt-data":receiptString, "password":SUBSCRIPTION_SECRET] as [String : Any]
var jsonData:Data?
do{
jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
}
catch{
print("ERROR: " + error.localizedDescription)
}
let storeURL = NSURL(string:"https://sandbox.itunes.apple.com/verifyReceipt")!
let storeRequest = NSMutableURLRequest(url: storeURL as URL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = jsonData!
let session = URLSession(configuration:URLSessionConfiguration.default)
let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error in
do{
let jsonResponse:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
let expirationDate:NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse)!
self.updateIAPExpirationDate(date: expirationDate)
}
catch{
print("ERROR: " + error.localizedDescription)
}
}
task.resume()
}
}
However, when I run the app with this code, it hangs upon reaching the line jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
. It doesn't even make it to the catch block, it just stops doing anything. From what I've seen online, other people seem to have trouble using JSONSerialization.data to set the request httpBody in Swift 3.
Use the production URL https://buy.itunes.apple.com/verifyReceipt when your app is live in the App Store. For more information on these endpoints, see verifyReceipt. Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code.
Receipt verification is a systematic process by which a site verifies that what was specified and ordered is actually what is received.
Its working correctly with Swift 4
func receiptValidation() {
let SUBSCRIPTION_SECRET = "yourpasswordift"
let receiptPath = Bundle.main.appStoreReceiptURL?.path
if FileManager.default.fileExists(atPath: receiptPath!){
var receiptData:NSData?
do{
receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
}
catch{
print("ERROR: " + error.localizedDescription)
}
//let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn)
print(base64encodedReceipt!)
let requestDictionary = ["receipt-data":base64encodedReceipt!,"password":SUBSCRIPTION_SECRET]
guard JSONSerialization.isValidJSONObject(requestDictionary) else { print("requestDictionary is not valid JSON"); return }
do {
let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt" // this works but as noted above it's best to use your own trusted server
guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
let session = URLSession(configuration: URLSessionConfiguration.default)
var request = URLRequest(url: validationURL)
request.httpMethod = "POST"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
if let data = data , error == nil {
do {
let appReceiptJSON = try JSONSerialization.jsonObject(with: data)
print("success. here is the json representation of the app receipt: \(appReceiptJSON)")
// if you are using your server this will be a json representation of whatever your server provided
} catch let error as NSError {
print("json serialization failed with error: \(error)")
}
} else {
print("the upload task returned an error: \(error)")
}
}
task.resume()
} catch let error as NSError {
print("json serialization failed with error: \(error)")
}
}
}
I have updated the @user3726962's code, removing unnecessary NS'es and "crash operators". It should look more like Swift 3 now.
Before using this code be warned that Apple doesn't recommend doing direct [device] <-> [Apple server] validation and asks to do it [device] <-> [your server] <-> [Apple server]. Use only if you are not afraid to have your In-App Purchases hacked.
UPDATE: Made the function universal: it will attempt to validate receipt with Production first, if fails - it will repeat with Sandbox. It's a bit bulky, but should be quite self-contained and independent from 3rd-parties.
func tryCheckValidateReceiptAndUpdateExpirationDate() {
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
NSLog("^A receipt found. Validating it...")
GlobalVariables.isPremiumInAmbiquousState = true // We will allow user to use all premium features until receipt is validated
// If we have problems validating the purchase - this is not user's fault
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
let dict = ["receipt-data" : receiptString, "password" : "your_shared_secret"] as [String : Any]
do {
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
if let storeURL = Foundation.URL(string:"https://buy.itunes.apple.com/verifyReceipt"),
let sandboxURL = Foundation.URL(string: "https://sandbox.itunes.apple.com/verifyReceipt") {
var request = URLRequest(url: storeURL)
request.httpMethod = "POST"
request.httpBody = jsonData
let session = URLSession(configuration: URLSessionConfiguration.default)
NSLog("^Connecting to production...")
let task = session.dataTask(with: request) { data, response, error in
// BEGIN of closure #1 - verification with Production
if let receivedData = data, let httpResponse = response as? HTTPURLResponse,
error == nil, httpResponse.statusCode == 200 {
NSLog("^Received 200, verifying data...")
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject>,
let status = jsonResponse["status"] as? Int64 {
switch status {
case 0: // receipt verified in Production
NSLog("^Verification with Production succesful, updating expiration date...")
self.updateExpirationDate(jsonResponse: jsonResponse) // Leaves isPremiumInAmbiquousState=true if fails
case 21007: // Means that our receipt is from sandbox environment, need to validate it there instead
NSLog("^need to repeat evrything with Sandbox")
var request = URLRequest(url: sandboxURL)
request.httpMethod = "POST"
request.httpBody = jsonData
let session = URLSession(configuration: URLSessionConfiguration.default)
NSLog("^Connecting to Sandbox...")
let task = session.dataTask(with: request) { data, response, error in
// BEGIN of closure #2 - verification with Sandbox
if let receivedData = data, let httpResponse = response as? HTTPURLResponse,
error == nil, httpResponse.statusCode == 200 {
NSLog("^Received 200, verifying data...")
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject>,
let status = jsonResponse["status"] as? Int64 {
switch status {
case 0: // receipt verified in Sandbox
NSLog("^Verification succesfull, updating expiration date...")
self.updateExpirationDate(jsonResponse: jsonResponse) // Leaves isPremiumInAmbiquousState=true if fails
default: self.showAlertWithErrorCode(errorCode: status)
}
} else { DebugLog("Failed to cast serialized JSON to Dictionary<String, AnyObject>") }
}
catch { DebugLog("Couldn't serialize JSON with error: " + error.localizedDescription) }
} else { self.handleNetworkError(data: data, response: response, error: error) }
}
// END of closure #2 = verification with Sandbox
task.resume()
default: self.showAlertWithErrorCode(errorCode: status)
}
} else { DebugLog("Failed to cast serialized JSON to Dictionary<String, AnyObject>") }
}
catch { DebugLog("Couldn't serialize JSON with error: " + error.localizedDescription) }
} else { self.handleNetworkError(data: data, response: response, error: error) }
}
// END of closure #1 - verification with Production
task.resume()
} else { DebugLog("Couldn't convert string into URL. Check for special characters.") }
}
catch { DebugLog("Couldn't create JSON with error: " + error.localizedDescription) }
}
catch { DebugLog("Couldn't read receipt data with error: " + error.localizedDescription) }
} else {
DebugLog("No receipt found even though there is an indication something has been purchased before")
NSLog("^No receipt found. Need to refresh receipt.")
self.refreshReceipt()
}
}
func refreshReceipt() {
let request = SKReceiptRefreshRequest()
request.delegate = self // to be able to receive the results of this request, check the SKRequestDelegate protocol
request.start()
}
This works for auto-renewable subscriptions. Haven't tested it with other kinds of subscriptions yet. Leave a comment if it works for you with some other subscription type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With