Reputation: 343
I am developing an iOS app in Swift and attempting to implement receipt validation for an in-app purchase. I couldn't figure out how to achieve this in Swift, so instead I tried having my app send the request through a Lambda function writtin in Node.js, after seeing Giulio Roggero's example in this question. My Swift code looks like this:
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: .endLineWithLineFeed)
let invocationRequest = AWSLambdaInvokerInvocationRequest()
invocationRequest?.functionName = "sendReceiptRequest"
invocationRequest?.invocationType = AWSLambdaInvocationType.requestResponse
invocationRequest?.payload = ["receipt-data" : receiptString!, "password" : SUBSCRIPTION_SECRET]
let lambdaInvoker = AWSLambdaInvoker.default()
lock()
lambdaInvoker.invoke(invocationRequest!).continue(with: AWSExecutor.mainThread(), with: { (task:AWSTask!) -> AnyObject! in
if task.error != nil {
self.sendErrorPopup("Error: \(task.error?.localizedDescription)")
} else {
print("TOKEN: ", task.result)
}
self.unlock()
return nil
})}
My Lambda node.js function looks like this, following the example:
function (receiptData_base64, password, production, cb)
{
var url = production ? 'buy.itunes.apple.com' : 'sandbox.itunes.apple.com'
var receiptEnvelope = {
"receipt-data": receiptData_base64,
"password":password
};
var receiptEnvelopeStr = JSON.stringify(receiptEnvelope);
var options = {
host: url,
port: 443,
path: '/verifyReceipt',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(receiptEnvelopeStr)
}
};
var req = https.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log("body: " + chunk);
cb(true, chunk);
});
res.on('error', function (error) {
console.log("error: " + error);
cb(false, error);
});
});
req.write(receiptEnvelopeStr);
req.end();
}
However, when running this code, either through a lambda test or through my app, I get an error message that simply says Response body:
{"errorMessage":"true"}
. I've noticed that if I tweak the code I can create more expected errors- For instance, if I have some other value for the receipt-data, I get a 21002 error code in response, and if I change "production" to true, I get a 21007 error. Part of the problem is that I don't know exactly how the callback is supposed to work-- is the block inside https.request
correct for what I'm trying to do in Swift? I get the impression that the receipt-data is correctly formatted since changing it yields a different result, so why is the end result still an error?
EDIT:
Something I previously didn't notice is that when I run the Lambda function, the line "body: (receipt data)" appears, where (receipt data) is the base 64 encoded data I sent to the function. This makes me suspect I'm not reaching the error callback block at all, and that the error has something to do with the way I send the result of the callback back to my app. What is this block:
var req = https.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log("body: " + chunk);
cb(true, chunk);
});
res.on('error', function (error) {
console.log("error: " + error);
cb(false, error);
});
});
supposed to do? Is it possible I need to enable some permission to receive the callback?
Upvotes: 2
Views: 2869
Reputation: 186
Here's what worked for me:
'use strict'
var AWS = require('aws-sdk')
var https = require('https')
exports.handler = (event, context, callback) => {
var payload = JSON.stringify({
'receipt-data': event.arguments.input.base64_receipt
})
function generateOptions(url) {
return {
host: url,
port: 443,
path: '/verifyReceipt',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
}
}
}
function validateReceipt(url, payload, errorCallback) {
var req = https.request(generateOptions(url), function(res) {
res.setEncoding('utf8')
res.on('data', function(chunk) {
var data = JSON.parse(chunk)
// success
callback(null, event)
})
res.on('error', function(error) {
//error
errorCallback ? errorCallback() : callback('There was an error validating the transaction', event)
})
})
req.write(payload)
req.end()
}
// attempt to validate on production and sandbox
validateReceipt('buy.itunes.apple.com', payload, function() {
validateReceipt('sandbox.itunes.apple.com', payload)
})
}
Upvotes: 1
Reputation: 63
In your Swift code, you check "if task.error != nil" then you have an error
lambdaInvoker.invoke(invocationRequest!).continue(with: AWSExecutor.mainThread(), with: { (task:AWSTask!) -> AnyObject! in
if task.error != nil {
self.sendErrorPopup("Error: \(task.error?.localizedDescription)")
} else {
print("TOKEN: ", task.result)
}
but in your node code, you call the callback that way:
cb(true, chunk);
The first parameter is your error which is not going to be nil. you should replace this line with:
cb(null, chunk);
You are going to get your correct Json that way
Upvotes: 0
Reputation: 44876
For anyone finding this later, potential problems:
Content-Type
is incorrect. You need to post JSON, not url encoding; while the code does that, it uses the wrong type to tell the server what format it's using. It should be application/json
not application/x-www-form-urlencoded
..endLineWithLineFeed
makes me suspicious.status
of 21007
, the code should then try the same receipt against the sandbox server. Once a receipt has been decoded and verified, the client can check the receipt to see which server it was verified against and (as necessary) ignore sandbox receipts.Upvotes: 2
Reputation: 343
Ultimately I solved the problem by following this example and using Python instead of Node.js. I still don't know exactly what was wrong with my code before, but base-64 encoding in Swift and then sending the receipt data and password to a Python Lambda function gave me the correct result.
Upvotes: 0