in-app-purchase
Version:
In-App-Purchase validation and subscription management for iOS, Android, Amazon, and Windows
908 lines (785 loc) • 30.7 kB
JavaScript
var constants = require('../constants');
var fs = require('fs');
var crypto = require('crypto');
var async = require('./async');
var request = require('request');
var responseData = require('./responseData');
var verbose = require('./verbose');
var googleApi = require('./googleAPI');
var testMode = false;
var useGoogleApi = false;
var sandboxPkey = 'iap-sandbox';
var livePkey = 'iap-live';
var config = null;
var keyPathMap = {};
var publicKeyMap = {};
var googleTokenMap = {};
var checkSubscriptionState = false;
var KEYS = {
ACCESS_TOKEN: 'access_token',
GRANT_TYPE: 'grant_type',
CLIENT_ID: 'client_id',
CLIENT_SECRET: 'client_secret',
REFRESH_TOKEN: 'refresh_token'
};
var ENV_PUBLICKEY = {
SANDBOX: 'GOOGLE_IAB_PUBLICKEY_SANDBOX',
LIVE: 'GOOGLE_IAB_PUBLICKEY_LIVE'
};
var NAME = '<Google>';
// See `cancelReason` at
// https://developers.google.com/android-publisher/api-ref/purchases/subscriptions
var CANCELLATION_REASON = {
// User cancelled the subscription. The subscription remains valid until its expiration time.
USER_CANCELLED: 0,
// Subscription was cancelled by the system, for example because of a billing problem.
SYSTEM_CANCELLED: 1,
// Subscription was replaced with a new subscription.
REPLACED: 2,
// Subscription was cancelled by the developer.
DEVELOPER_CANCELLED: 3
};
function isValidConfigKey(key) {
return key.match(/^google/);
}
// test use only
module.exports.reset = function () {
config = null;
keyPathMap = {};
publicKeyMap = {};
googleTokenMap = {};
checkSubscriptionState = false;
};
module.exports.readConfig = function (configIn) {
if (!configIn) {
// no google iap or public key(s) from ENV variables
return;
}
// apply configurations
if (configIn.requestDefaults) {
request = request.defaults(configIn.requestDefaults);
}
verbose.setup(configIn);
// we do NOT use live if true
testMode = configIn.test || false;
verbose.log(NAME, 'test mode?', testMode);
config = {};
var configValueSet = false;
Object.keys(configIn).forEach(function (key) {
if (isValidConfigKey(key)) {
config[key] = configIn[key];
configValueSet = true;
}
});
// backward compatibility
if (configIn && configIn.publicKeyStrSandbox) {
config.googlePublicKeyStrSandbox = configIn.publicKeyStrSandbox;
}
if (configIn && configIn.publicKeyStrLive) {
config.googlePublicKeyStrLive = configIn.publicKeyStrLive;
}
if (!configValueSet) {
config = null;
return;
}
keyPathMap.sandbox = config.googlePublicKeyPath + sandboxPkey;
keyPathMap.live = config.googlePublicKeyPath + livePkey;
// access token is not used for subscription validation
if (config.googleAccToken) {
googleTokenMap.accessToken = config.googleAccToken;
}
if (config.googleRefToken && config.googleClientID && config.googleClientSecret) {
googleTokenMap.refreshToken = config.googleRefToken;
googleTokenMap.clientID = config.googleClientID;
googleTokenMap.clientSecret = config.googleClientSecret;
checkSubscriptionState = true;
}
// googleServiceAccount { client_email, private_key } from Google API service account JSON file
if (config.googleServiceAccount) {
useGoogleApi = true;
googleApi.config(config.googleServiceAccount);
}
};
module.exports.setup = function (cb) {
if (config && (config.googlePublicKeyStrSandbox || config.googlePublicKeyStrLive || config.googleAPIKeyData)) {
// try to read public key value as string
if (config && config.googlePublicKeyStrSandbox) {
publicKeyMap.sandbox = config.googlePublicKeyStrSandbox;
}
if (config && config.googlePublicKeyStrLive) {
publicKeyMap.live = config.googlePublicKeyStrLive;
}
return cb();
}
if (!config || !config.googlePublicKeyPath) {
// try to read public key value from ENV if available
// if this is set, reading the public key value from file system is ignored
if (process.env[ENV_PUBLICKEY.SANDBOX]) {
publicKeyMap.sandbox = process.env[ENV_PUBLICKEY.SANDBOX].replace(/s+$/, '');
}
if (process.env[ENV_PUBLICKEY.LIVE]) {
publicKeyMap.live = process.env[ENV_PUBLICKEY.LIVE].replace(/s+$/, '');
}
return cb();
}
var keys = Object.keys(keyPathMap);
async.eachSeries(keys, function (key, next) {
var pkeyPath = keyPathMap[key];
fs.readFile(pkeyPath, function (error, fileData) {
// we are ignoring missing public key file(s)
if (error) {
return next();
}
publicKeyMap[key] = fileData.toString().replace(/\s+$/, '');
next();
});
}, cb);
};
// receipt is an object
/*
* receipt = { receipt: 'stringified receipt data', signature: 'receipt signature' };
* if receipt.data is an object, it silently stringifies it
*/
module.exports.validatePurchase = function (dPubkey, receipt, cb) {
verbose.log(NAME, 'Validate this:', receipt);
if (useGoogleApi) {
/**
* receipt {
packageName: <string>,
productId: <string>
purchaseToken: <string>,
subscription: <bool>
}
*/
return googleApi.validatePurchase(dPubkey, receipt, cb);
} else if (dPubkey && dPubkey.clientEmail && dPubkey.privateKey) {
/**
* receipt {
packageName: <string>,
productId: <string>
purchaseToken: <string>,
subscription: <bool>
}
*/
return googleApi.validatePurchase(dPubkey, receipt, cb);
}
if (typeof receipt !== 'object') {
verbose.log(NAME, 'Failed: malformed receipt');
return cb(new Error('malformed receipt: ' + receipt), {
status: constants.VALIDATION.FAILURE,
message: 'Malformed receipt'
});
}
// the data might be in receipt and not in data
if (receipt.receipt && !receipt.data) {
receipt.data = receipt.receipt;
}
if (!receipt.data || !receipt.signature) {
verbose.log(NAME, 'Failed: missing receipt content');
return cb(new Error('missing receipt data:\n' + JSON.stringify(receipt)), {
status: constants.VALIDATION.FAILURE,
message: 'Malformed receipt'
});
}
if (typeof receipt.data === 'object') {
// stringify and make sure to escpace the value of developerPayload
receipt.data = JSON.stringify(receipt.data).replace(/\//g, '\\/');
verbose.log(NAME, 'Auto stringified receipt data:', receipt.data);
}
var pubkey = publicKeyMap.live;
var tokenMap = {
clientId: googleTokenMap.clientID,
clientSecret: googleTokenMap.clientSecret,
refreshToken: googleTokenMap.refreshToken
};
// override pubkey to allow dynamically fed public key to validate
if (typeof dPubkey === 'string') {
verbose.log(NAME, 'Using dynamically fed public key:', dPubkey);
pubkey = dPubkey;
}
// dPubkey can be and object w/ clientId, clientSecret, and refreshToken
if (dPubkey && typeof dPubkey === 'object') {
if (dPubkey.clientId && dPubkey.clientSecret && dPubkey.refreshToken) {
verbose.log(NAME, 'Using dynamically fed google client ID, client secret, and refresh token:', dPubkey);
// override tokenMap
tokenMap.clientId = dPubkey.clientId;
tokenMap.clientSecret = dPubkey.clientSecret;
tokenMap.refreshToken = dPubkey.refreshToken;
}
}
// if we have unencoded receipt signature...
if (receipt.signature.indexOf(' ') > -1) {
receipt.signature = receipt.signature.replace(/\ /g, '+');
}
var validateMethod;
// decide which method to use for validation
if (tokenMap.clientId && tokenMap.clientSecret && tokenMap.refreshToken) {
verbose.log(NAME, 'Validation w/ Google Play API');
// use newer Google API
validateMethod = function (_cb) {
validateProduct(receipt, tokenMap, _cb);
};
} else {
verbose.log(NAME, 'Validation w/ public key');
validateMethod = function (_cb) {
validatePublicKey(receipt, getPublicKey(pubkey), function (error, data) {
if (error) {
// if the receipt is a subscription, we validate w/ Google API instead
// checkSubscriptionStatus() is most likely unnecessary now
if (isSubscription(receipt)) {
return validateSubscription(receipt, tokenMap, cb);
}
}
_cb(error, data);
});
};
}
verbose.log(NAME, 'Try validate against live public key:', pubkey);
if (testMode) {
// try sandbox only
validateMethod(function (error, data) {
if (error) {
verbose.log(NAME, 'Failed against sandbox public key:', error);
// we will send the error from live only
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
verbose.log(NAME, 'Validation against sandbox public key successful:', data);
// sandbox worked
checkSubscriptionStatus(data, cb);
});
return;
}
// try live first
validateMethod(function (error, data) {
if (error) {
if (!publicKeyMap.sandbox) {
verbose.log(NAME, 'Failed to validate against:', pubkey, error);
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
pubkey = publicKeyMap.sandbox;
verbose.log(NAME, 'Failed against live public key:', error);
verbose.log(NAME, 'Try validate against sandbox public key:', pubkey);
// now try sandbox
// we are stringifying receipt to b/c we already parsed receipt...
receipt.data = JSON.stringify(receipt.data).replace(/\//g, '\\/');
validateMethod(function (error2, data) {
if (error2) {
verbose.log(NAME, 'Failed against sandbox public key:', error2);
// we will send the error from live only
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
verbose.log(NAME, 'Validation against sandbox public key successful:', data);
// this here maybe deprecated b/c we now have subscriotion validation before this
// sandbox worked
checkSubscriptionStatus(data, cb);
});
return;
}
verbose.log(NAME, 'Validation against live public key successful:', data);
// live worked
// this here maybe deprecated b/c we now have subscription validation before this
checkSubscriptionStatus(data, cb);
});
};
function isSubscription(receipt) {
if (typeof receipt === 'string') {
try {
receipt = JSON.parse(receipt);
return receipt.data && receipt.data.autoRenewing !== undefined;
} catch (error) {
return false;
}
}
if (typeof receipt.data === 'string') {
try {
receipt.data = JSON.parse(receipt.data);
return receipt.data && receipt.data.autoRenewing !== undefined;
} catch (error) {
return false;
}
}
return receipt.data && receipt.data.autoRenewing !== undefined;
}
function validateProduct(receipt, tokenMap, cb) {
// if the receipt is a subscription, we validate w/ Google API instead
// checkSubscriptionStatus() is most likely unnecessary now
if (isSubscription(receipt)) {
return validateSubscription(receipt, tokenMap, cb);
}
if (typeof receipt === 'string') {
try {
receipt = JSON.parse(receipt);
} catch (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
}
if (typeof receipt.data === 'string') {
try {
receipt.data = JSON.parse(receipt.data);
} catch (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
}
verbose.log(NAME, 'Validate purchase as product w/:', tokenMap);
if (!tokenMap.clientId || !tokenMap.clientSecret || !tokenMap.refreshToken) {
return cb(new Error('missing google play api info'), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: 'client_id, client_secret, access_token and refres_token should be provided'
}));
}
auth(tokenMap, function (error, body) {
if (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
verbose.log(NAME, 'Google API authenticated', body.body);
// well....
var accessToken = body.access_token || body.body.access_token;
verbose.log(NAME, 'Google API access token:', accessToken);
if (!accessToken) {
return cb(new Error(JSON.stringify(body)), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: 'failed to authenticate api call'
}));
}
var url = 'https://www.googleapis.com/androidpublisher/v3/applications/' +
encodeURIComponent(receipt.data.packageName) +
'/purchases/products/' +
encodeURIComponent(receipt.data.productId) +
'/tokens/' + encodeURIComponent(receipt.data.purchaseToken) +
'?access_token=' + encodeURIComponent(accessToken);
request.get({
url: url,
json: true
}, _onProductValidate);
});
function _onProductValidate(error, res, body) {
if (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
if (res.statusCode === 410) {
// https://stackoverflow.com/questions/45688494/google-android-publisher-api-responds-with-410-purchasetokennolongervalid-erro
verbose.log(NAME, 'Receipt is no longer valid');
return cb(new Error('ReceiptNoLongerValid'), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: body
}));
}
if (res.statusCode >= 399) {
verbose.log(NAME, 'Product validation failed:', body, res.statusCode);
return cb(new Error('Status:' + res.statusCode), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: body
}));
}
if (body.error && body.errors.length > 0) {
verbose.log(NAME, 'Product validation failed with error:', body.error);
return cb(new Error(body.error.message), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: body.error.message
}));
}
if (body.purchaseState !== 0) {
verbose.log(NAME, 'Purchase has been canceled:', body);
return cb(new Error('PurchaseCanceled'), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: 'PurchaseCanceled'
}));
}
verbose.log(NAME, 'Product purchase validated:', body);
var resp = receipt.data;
resp.status = constants.VALIDATION.SUCCESS;
for (var key in body) {
resp[key] = body[key];
}
// we need service
resp.service = constants.SERVICES.GOOGLE;
cb(null, resp);
}
}
function validateSubscription(receipt, tokenMap, cb) {
if (typeof receipt === 'string') {
try {
receipt = JSON.parse(receipt);
} catch (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
}
if (typeof receipt.data === 'string') {
try {
receipt.data = JSON.parse(receipt.data);
} catch (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
}
verbose.log(NAME, 'Validate purchase as subscription w/:', tokenMap);
if (!tokenMap.clientId || !tokenMap.clientSecret || !tokenMap.refreshToken) {
return cb(new Error('missing google play api info'), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: 'client_id, client_secret, access_token and refres_token should be provided'
}));
}
auth(tokenMap, function (error, res) {
if (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
verbose.log(NAME, 'Google API authenticated', res.body);
// well....
var accessToken = res.access_token || res.body.access_token;
verbose.log(NAME, 'Google API access token:', accessToken);
if (!accessToken) {
return cb(new Error(JSON.stringify(res.body || res)), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: 'failed to authenticate api call'
}));
}
var url = 'https://www.googleapis.com/androidpublisher/v3/applications/' +
encodeURIComponent(receipt.data.packageName) +
'/purchases/subscriptions/' +
encodeURIComponent(receipt.data.productId) +
'/tokens/' + encodeURIComponent(receipt.data.purchaseToken) +
'?access_token=' + encodeURIComponent(accessToken);
request.get({
url: url,
json: true
}, _onSubscriptionValidate);
});
function _onSubscriptionValidate(error, res, body) {
if (error) {
return cb(error, createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
if (res.statusCode >= 399) {
verbose.log(NAME, 'Product validation failed:', body, res.statusCode);
return cb(new Error('Status:' + res.statusCode), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: body
}));
}
if (body.error && body.errors.length > 0) {
verbose.log(NAME, 'Product validation failed with error:', body.error);
return cb(new Error(body.error.message), createErrorData(receipt, {
status: constants.VALIDATION.FAILURE,
message: body.error.message
}));
}
verbose.log(NAME, 'Subscription purchase validated:', body);
var resp = receipt.data;
resp.status = constants.VALIDATION.SUCCESS;
for (var key in body) {
resp[key] = body[key];
}
// we need service
resp.service = constants.SERVICES.GOOGLE;
cb(null, resp);
}
}
module.exports.getPurchaseData = function (purchase, options) {
if (!purchase) {
return null;
}
if (options && options.ignoreExpired) {
var now = Date.now();
if (purchase.expiryTimeMillis <= now) {
return [];
} else if (purchase.expirationTime <= now) {
return [];
} else if (
purchase.cancelReason &&
purchase.cancelReason !== CANCELLATION_REASON.USER_CANCELLED
) {
// User cancellations do not not void the subscription.
return [];
}
}
var data = [];
var purchaseInfo = responseData.parse(purchase);
purchaseInfo.transactionId = purchase.purchaseToken;
purchaseInfo.purchaseDate = purchase.purchaseTime;
purchaseInfo.quantity = 1;
if (checkSubscriptionState) {
purchaseInfo.expirationDate = purchase.expirationTime;
}
if (purchase.expiryTimeMillis) {
purchaseInfo.expirationDate = purchase.expiryTimeMillis;
}
// User cancellations do not not void the subscription.
// See `userCancellationTimeMillis` at
// https://developers.google.com/android-publisher/api-ref/purchases/subscriptions
if (
purchase.cancelReason &&
purchase.cancelReason !== CANCELLATION_REASON.USER_CANCELLED
) {
// Artificially promoting the expiryTimeMillis to the cancellation date for
// use by the isExpired utility function. Google does not provide a cancellation
// date for non-user cancelled reasons.
purchaseInfo.cancellationDate = purchase.expiryTimeMillis;
}
data.push(purchaseInfo);
return data;
};
/** Deprecated as of Aug 1st 20017 b/c we now have validateSubscription()
* Function to check subscription status in Google Play
* @param {Object} data receipt data
* @param {Function} cb callback function
*/
function checkSubscriptionStatus(data, cb) {
data.service = constants.SERVICES.GOOGLE;
if (!checkSubscriptionState) {
return cb(null, data);
}
var packageName = data.packageName;
var subscriptionID = data.productId;
var purchaseToken = data.purchaseToken;
var urlPurchaseTypeSegment = data.autoRenewing === undefined ? 'products' : 'subscriptions';
var url = 'https://www.googleapis.com/androidpublisher/v3/applications/' + packageName +
'/purchases/' + urlPurchaseTypeSegment + '/' + subscriptionID + '/tokens/' + purchaseToken;
var state;
var mustRecheck = false;
var getSubInfo = function (next) {
verbose.log(NAME, 'Get subscription info from', url);
getSubscriptionInfo(url, function (error, response, body) {
if (error || 'error' in body) {
verbose.log(NAME, 'Failed to get subscription info from', url, error, body);
state = constants.VALIDATION.FAILURE;
// we must move on to validate()
next();
return;
}
_copyPurchaseDetails(data, body);
state = constants.VALIDATION.SUCCESS;
verbose.log(NAME, 'Successfully retrieved subscription info from', url, data);
next();
});
};
var validate = function (next) {
switch (state) {
case constants.VALIDATION.SUCCESS:
// This line tells the next function there is no need to get subscription Info again.
// We should read this as a "No, don't call that function again"
verbose.log(NAME, 'Validated successfully');
next(null, constants.VALIDATION.FAILURE);
break;
case constants.VALIDATION.FAILURE:
verbose.log(NAME, 'Refresh Google token');
refreshGoogleTokens(function (error, res, body) {
if (error) {
verbose.log(NAME, 'Failed to refresh Google token:', error);
return cb(error, createErrorData({ data: data }, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
var parsedBody = JSON.parse(body);
if ('error' in parsedBody) {
verbose.log(NAME, 'Failed to refresh Google token:', parsedBody);
return cb(new Error(parsedBody.error), createErrorData({ data: data }, {
status: constants.VALIDATION.FAILURE,
message: parsedBody.error
}));
}
// Store new access token
googleTokenMap.accessToken = parsedBody[KEYS.ACCESS_TOKEN];
state = constants.VALIDATION.SUCCESS;
// On the other hand, here we are telling the next function
// to get subscription Info again.
mustRecheck = true;
verbose.log(NAME, 'Successfully refreshed Google token:', googleTokenMap.accessToken);
next();
});
break;
}
};
var recheck = function (next) {
if (state === constants.VALIDATION.SUCCESS) {
if (!mustRecheck) {
// we are successful and done
return next();
}
verbose.log(NAME, 'Re-check subscription info:', url);
getSubscriptionInfo(url, function (error, response, body) {
if (error || 'error' in body) {
verbose.log(NAME, 'Re-check failed:', url, error, body);
state = constants.VALIDATION.FAILURE;
next(error ? error : new Error(body.error));
return;
}
_copyPurchaseDetails(data, body);
state = constants.VALIDATION.SUCCESS;
verbose.log(NAME, 'Re-check successfully retrieved subscription info:', url, data);
next();
});
return;
}
// refresh failed
state = constants.VALIDATION.FAILURE;
next();
};
var done = function (error) {
if (error) {
return cb(error, createErrorData({ data: data }, {
status: constants.VALIDATION.FAILURE,
message: error.message
}));
}
cb(null, data);
};
var tasks = [
getSubInfo,
validate,
recheck
];
async.series(tasks, done);
}
/** Deprecated as of Aug 1st 20017 b/c we now have validateSubscription()
*/
function _copyPurchaseDetails(data, body) {
data.purchaseState = body.purchaseState;
data.autoRenewing = body.autoRenewing;
data.expirationTime = body.expiryTimeMillis;
data.paymentState = body.paymentState;
data.priceCurrencyCode = body.priceCurrencyCode;
data.priceAmountMicros = body.priceAmountMicros;
data.cancelReason = body.cancelReason;
data.countryCode = body.countryCode;
}
function getPublicKey(publicKey) {
if (!publicKey) {
return null;
}
var key = chunkSplit(publicKey, 64, '\n');
var pkey = '-----BEGIN PUBLIC KEY-----\n' + key + '-----END PUBLIC KEY-----\n';
return pkey;
}
function validatePublicKey(receipt, pkey, cb) {
if (!receipt || !receipt.data) {
return cb(new Error('missing receipt data'));
}
if (!pkey) {
return cb(new Error('missing public key'));
}
if (typeof receipt.data !== 'string') {
return cb(new Error('receipt.data must be a string'));
}
var validater = crypto.createVerify('SHA1');
var valid;
validater.update(receipt.data);
try {
valid = validater.verify(pkey, receipt.signature, 'base64');
} catch (error) {
return cb(error);
}
if (valid) {
// validated successfully
var data = JSON.parse(receipt.data);
data.status = constants.VALIDATION.SUCCESS;
return cb(null, data);
}
// failed to validate
cb(new Error('failed to validate purchase'));
}
function chunkSplit(str, len, end) {
len = parseInt(len, 10) || 76;
if (len < 1) {
return false;
}
end = end || '\r\n';
return str.match(new RegExp('.{0,' + len + '}', 'g')).join(end);
}
function getSubscriptionInfo(url, cb) {
var options = {
method: 'GET',
url: url,
headers: {
'Authorization': 'Bearer ' + googleTokenMap.accessToken
},
json: true
};
request(options, cb);
}
module.exports.refreshToken = function (cb) {
if (!checkSubscriptionState) {
return cb(new Error('missing google play api info'), {
status: constants.VALIDATION.FAILURE,
message: 'client_id, client_secret, access_token and refres_token should be provided'
});
}
refreshGoogleTokens(function (error, res, body) {
if (error) {
return cb(error, { status: constants.VALIDATION.FAILURE, message: error.message });
}
var parsedBody = JSON.parse(body);
if ('error' in parsedBody) {
return cb(new Error(parsedBody.error), {
status: constants.VALIDATION.FAILURE,
message: parsedBody.error
});
}
// Store new access token
googleTokenMap.accessToken = parsedBody[KEYS.ACCESS_TOKEN];
cb(null, parsedBody);
});
};
function refreshGoogleTokens(cb) {
var body = {};
body[KEYS.GRANT_TYPE] = KEYS.REFRESH_TOKEN;
body[KEYS.CLIENT_ID] = googleTokenMap.clientID;
body[KEYS.CLIENT_SECRET] = googleTokenMap.clientSecret;
body[KEYS.REFRESH_TOKEN] = googleTokenMap.refreshToken;
var options = {
method: 'POST',
url: 'https://accounts.google.com/o/oauth2/token',
form: body
};
request(options, cb);
}
// redundant..... :(
function auth(tokenMap, cb) {
var body = {};
body[KEYS.GRANT_TYPE] = KEYS.REFRESH_TOKEN;
body[KEYS.CLIENT_ID] = tokenMap.clientId;
body[KEYS.CLIENT_SECRET] = tokenMap.clientSecret;
body[KEYS.REFRESH_TOKEN] = tokenMap.refreshToken;
var options = {
method: 'POST',
url: 'https://accounts.google.com/o/oauth2/token',
form: body,
json: true
};
request(options, cb);
}
function createErrorData(receipt, obj) {
obj.data = receipt.data || null;
return obj;
}