diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index e51df40..de3fe4e 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,16 @@ written by Paul Crawford, I wanted a pure JavaScript implementation of in-app pu I also wanted to add support for other app stores, and not just limit this to Apple. The `iap` module is that, and will be extended to app stores other than Apple's (pull requests welcome!). + + ## Installation ```sh npm install iap ``` + + ## Usage Only a single method is exposed to verify purchase receipts: @@ -33,9 +37,11 @@ iap.verifyPayment(platform, payment, function (error, response) { The receipt you pass must conform to the requirements of the backend you are verifying with. Read the next chapter for more information on the format. + + ## Supported platforms -### Apple +### Apple Store **The payment object** @@ -75,6 +81,14 @@ will be in the result object: } ``` + +### Google Play + +**The payment object** + +**The response** + + Regardless of the platform used, besides the platform-specific receipt, the following properties will be included: @@ -82,6 +96,28 @@ will be included: * productId, which specifies what was purchased. * platform, which is always the platform you passed. + + ## License MIT + + + +## References + +### Apple Store References + + +### Google Play References + * https://bitbucket.org/gooroo175/google-play-purchase-validator/src/d88278c30df0d0dc51b852b7bcab5f40e3a30923/index.js?at=master + * https://github.com/machadogj/node-google-bigquery + * https://github.com/extrabacon/google-oauth-jwt/blob/master/lib/request-jwt.js + + * https://developer.android.com/google/play/billing/gp-purchase-status-api.html + * https://developers.google.com/android-publisher/getting_started + * https://developers.google.com/android-publisher/authorization + * https://developers.google.com/accounts/docs/OAuth2ServiceAccount + * https://developers.google.com/android-publisher/api-ref/purchases/products + * https://developers.google.com/android-publisher/api-ref/purchases/products/get + * http://developer.android.com/google/play/billing/billing_testing.html \ No newline at end of file diff --git a/index.js b/index.js index c853390..e8001e7 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ var platforms = { - apple: require('./lib/apple') + apple: require('./lib/apple'), + google: require('./lib/apple') }; diff --git a/lib/google/index.js b/lib/google/index.js new file mode 100644 index 0000000..5f514fe --- /dev/null +++ b/lib/google/index.js @@ -0,0 +1,38 @@ +var assert = require('assert'); +var jwt = require('./jwt'); +var apiUrls = require('./urls'); +var https = require('../https'); + + + +exports.verifyPayment = function (payment, cb) { + var data; + + try { + assert.equal(typeof payment.packageName, 'string', 'Package name must be a string'); + assert.equal(typeof payment.productId, 'string', 'Product ID must be a string'); + assert.equal(typeof payment.receipt, 'string', 'Receipt must be a string'); + assert.equal(typeof payment.iss, 'string', 'Google ISS must be a string'); + assert.equal(typeof payment.key, 'string', 'Private key must be a string'); + } catch (error) { + return process.nextTick(function () { + cb(error); + }); + } + + jwt.getToken(payment.iss, payment.key, apiUrls.publisherScope, function (error, requestToken) { + if (error) { + return cb(error); + } + + var requestUrl = apiUrls.purchasesProductsGet(payment.packageName, payment.productId, payment.receipt, requestToken.access_token); + https.get(requestUrl, null, function (error, responseString) { + if (error) { + return cb(error); + } + + console.log('\nResult:', responseString); + return cb(); + }); + }); +}; \ No newline at end of file diff --git a/lib/google/jwt.js b/lib/google/jwt.js new file mode 100644 index 0000000..d2fd94a --- /dev/null +++ b/lib/google/jwt.js @@ -0,0 +1,37 @@ +var jwt = require('jwt-simple'); +var apiUrls = require('./urls'); +var https = require('../https'); + + +var oneHour = 60 * 60; + + +exports.getToken = function (iss, key, scope, cb) { + var jwtToken = jwt.encode({ + iss: iss, + scope: scope, + aud: apiUrls.tokenRequest, + exp: Math.floor(Date.now() / 1000) + oneHour, + iat: Math.floor(Date.now() / 1000) + }, key, 'RS256'); + + var formData = { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: jwtToken + } + + https.post(apiUrls.tokenRequest, { form: formData }, function (error, responseString) { + if (error) { + return cb(error); + } + + var responseObject; + try { + responseObject = JSON.parse(responseString); + } catch (error) { + return cb(error); + } + + return cb(null, responseObject); + }); +} \ No newline at end of file diff --git a/lib/google/urls.js b/lib/google/urls.js new file mode 100644 index 0000000..03c701f --- /dev/null +++ b/lib/google/urls.js @@ -0,0 +1,23 @@ +var util = require('util'); + + +// OAuth URLs +exports.tokenRequest = 'https://accounts.google.com/o/oauth2/token'; + + +// Authentication scope URLs +exports.publisherScope = 'https://www.googleapis.com/auth/androidpublisher'; + + +// Android Purchases URLs & generators +exports.purchasesProductsGet = function (packageName, productId, receipt, accessToken) { + var baseUrl = 'https://www.googleapis.com/androidpublisher/v2'; + var packageUri = 'applications/' + encodeURIComponent(packageName); + var productUri = 'purchases/products/' + encodeURIComponent(productId); + var receiptUri = 'tokens/' + encodeURIComponent(receipt); + + var purchaseUrl = [baseUrl, packageUri, productUri, receiptUri].join('/'); + var accessToken = 'access_token=' + encodeURIComponent(accessToken); + + return purchaseUrl + '?' + accessToken; +}; \ No newline at end of file diff --git a/lib/https/get.js b/lib/https/get.js new file mode 100644 index 0000000..61b6eb4 --- /dev/null +++ b/lib/https/get.js @@ -0,0 +1,10 @@ +var https = require('./index'); + + +module.exports = function (url, options, cb) { + options = options || {}; + + // Set method to GET and call it + options.method = 'GET'; + https.request(url, options, null, cb); +}; \ No newline at end of file diff --git a/lib/https/index.js b/lib/https/index.js new file mode 100644 index 0000000..bfd4a25 --- /dev/null +++ b/lib/https/index.js @@ -0,0 +1,3 @@ +exports.request = require('./request'); +exports.post = require('./post'); +exports.get = require('./get'); \ No newline at end of file diff --git a/lib/https/post.js b/lib/https/post.js new file mode 100644 index 0000000..dac7f66 --- /dev/null +++ b/lib/https/post.js @@ -0,0 +1,26 @@ +var https = require('./index'); +var formUrlencoded = require('form-urlencoded'); + + +module.exports = function (url, options, cb) { + options = options || {}; + + // Try and extract data and set it's type + var data = null; + try { + if (options.form) { + data = formUrlencoded.encode(options.form); + delete options.form; + options.headers = { + 'content-type': 'application/x-www-form-urlencoded', + 'content-length': Buffer.byteLength(data) + }; + } + } catch (error) { + cb(error); + } + + // Set method to POST and call it + options.method = 'POST'; + https.request(url, options, data, cb); +}; \ No newline at end of file diff --git a/lib/https/request.js b/lib/https/request.js new file mode 100644 index 0000000..45956f4 --- /dev/null +++ b/lib/https/request.js @@ -0,0 +1,43 @@ +var url = require('url'); +var https = require('https'); + + +module.exports = function (requestUrl, options, data, cb) { + options = options || {}; + + var parsedUrl = url.parse(requestUrl); + + if (parsedUrl.hostname) { + options.hostname = parsedUrl.hostname; + } + + if (parsedUrl.port) { + options.port = parsedUrl.port; + } + + if (parsedUrl.path) { + options.path = parsedUrl.path; + } + + var req = https.request(options, function (res) { + res.setEncoding('utf8'); + + var responseData = ''; + + res.on('data', function (str) { + responseData += str; + }); + + res.on('end', function () { + if (res.statusCode !== 200) { + return cb(new Error('Received ' + res.statusCode + ' status code with body: ' + responseData)); + } + + cb(null, responseData); + }); + }); + + req.on('error', cb); + + req.end(data); +}; \ No newline at end of file diff --git a/package.json b/package.json index 2cb7ea4..c9f0b91 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "author": "Ron Korving ", "license": "MIT", "dependencies": { - "minimist": "0.0.8" + "minimist": "0.0.8", + "jwt-simple": "0.2.0", + "form-urlencoded": "0.0.6" } }