UNPKG

gracenode-iap

Version:

In-App-Purchase module for gracenode framework.

220 lines (198 loc) 5.71 kB
var gracenode = require('../gracenode'); var log = gracenode.log.create('iap'); var crypto = require('crypto'); var async = require('async'); var apple = require('./apple.js'); var google = require('./google.js'); var config = null; var mysql = null; // constants var VALIDATED = module.exports.VALIDATED = 'validated'; var ERROR = module.exports.ERROR = 'error'; var PENDING = module.exports.PENDING = 'pending'; var HANDLED = module.exports.HANDLED = 'handled'; var CANCELED = module.exports.CANCELED = 'canceled'; module.exports.readConfig = function (configIn) { if (!gracenode.mysql) { throw new Error('iap module requires gracenode-mysql module'); } if (!configIn || !configIn.sql) { throw new Error('invalid configurations given:\n' + JSON.stringify(configIn, null, 4)); } config = configIn; mysql = gracenode.mysql.create(config.sql); apple.readConfig(config); google.readConfig(config); }; module.exports.setup = function (cb) { if (!config.googlePublicKeyPath) { // no google iap return cb(); } google.setup(cb); }; // unit test functions module.exports.testApple = function (receipt, cb) { apple.validatePurchase(receipt, function (error, receipt, response) { if (error) { return cb(error); } try { var validateState = getValidateState(response); cb(null, { receiptHash: createReceiptHash(receipt), validateState: validateState, status: PENDING, response: response }); } catch (e) { return cb(e); } }); }; module.exports.testGoogle = function (receipt, cb) { google.validatePurchase(receipt, function (error, receipt, response) { if (error) { return cb(error); } try { var validateState = getValidateState(response); cb(null, { receiptHash: createReceiptHash(receipt), validateState: validateState, status: PENDING, response: response }); } catch (e) { return cb(e); } }); }; module.exports.isValidated = function (response) { if (response && response.validateState) { if (response.validateState === VALIDATED) { return true; } } return false; }; module.exports.validateApplePurchase = function (receipt, cb) { async.waterfall([ function (callback) { callback(null, receipt, cb); }, checkDb, apple.validatePurchase, storeResponse ], cb); }; module.exports.validateGooglePurchase = function (receipt, cb) { async.waterfall([ function (callback) { callback(null, receipt, cb); }, checkDb, google.validatePurchase, storeResponse ], cb); }; module.exports.updateStatus = function (receipt, status, cb) { if (status !== PENDING && status !== CANCELED && status !== HANDLED) { return cb(new Error('invalid status given: ' + status)); } var sql = 'UPDATE iap SET status = ?, modtime = ? WHERE receiptHashId = ? AND service = ?'; var service = (typeof receipt === 'object') ? 'google' : 'apple'; var params = [ status, new Date().getTime(), createReceiptHash(receipt), service ]; mysql.write(sql, params, function (error) { if (error) { return cb(error); } cb(); }); }; module.exports.createHashId = function (receipt) { return createReceiptHash(receipt); }; function checkDb(receipt, finalCallback, cb) { var sql = 'SELECT validateState, status, service, response FROM iap WHERE receiptHashId = ?'; var hash = createReceiptHash(receipt); var params = [hash]; mysql.searchOne(sql, params, function (error, res) { if (error) { return cb(error); } log.info('validated data in database: (receipt hash: ' + hash + ')', res); if (res) { // check status if (res.status === HANDLED || res.status === CANCELED) { // status is either handled or canceled > we do nothing in this case... return cb(new Error('status has already been ' + res.status)); } // check validation if (res.validateState === VALIDATED) { // this receipt has been validated by the service provider already try { res.response = JSON.parse(res.response); } catch (e) { log.error('response data corrupt:', res); return cb(new Error('response data corrupt')); } return finalCallback(null, res); } } cb(null, receipt); }); } // FIXME: on duplicate update... not really good. come up with a better way to handle this function storeResponse(receipt, response, validated, cb) { try { var validateState = getValidateState(response); var sql = 'INSERT INTO iap (receiptHashId, receipt, response, validateState, status, service, created, modtime) VALUES(?, ?, ?, ?, ?, ?, ?, ?)'; sql += ' ON DUPLICATE KEY UPDATE response = ?, validateState = ?, modtime = ?'; var resStr = JSON.stringify(response); var now = new Date().getTime(); var hash = createReceiptHash(receipt); var params = [ hash, prepareReceipt(receipt), resStr, validateState, PENDING, getService(receipt), now, now, resStr, validateState, now ]; mysql.write(sql, params, function (error) { if (error) { return cb(error); } cb(null, { receiptHash: hash, validateState: validateState, status: PENDING, response: response }); }); } catch (e) { return cb(e); } } function createReceiptHash(receipt) { // google's receipt is an object if (typeof receipt === 'object') { receipt = JSON.stringify(receipt); } var sha = crypto.createHash('sha256'); return sha.update(receipt.toString()).digest('hex'); } function prepareReceipt(receipt) { if (typeof receipt === 'object') { return JSON.stringify(receipt); } return receipt; } function getValidateState(response) { if (response && response.status === 0) { return VALIDATED; } return ERROR; } function getService(receipt) { if (typeof receipt === 'object') { return 'google'; } return 'apple'; }