in-app-purchase-iec-develop
Version:
In-App-Purchase validation and subscription management for iOS, Android, Amazon, and Windows
190 lines (169 loc) • 7.38 kB
JavaScript
var verbose = require('./verbose');
var constants = require('../constants');
var responseData = require('./responseData');
var request = require('request');
var crypto = require('crypto');
var urlbase64 = require('urlsafe-base64');
var FB = require('fb');
var fb = new FB.Facebook({ version: 'v3.3' });
var config = null;
function isValidConfigKey(key) {
return key.match(/^facebook/);
}
module.exports.readConfig = function (configIn) {
if (!configIn) {
// no facebook iap or password not required
return;
}
// set up verbose logging
verbose.setup(configIn);
config = {};
var configValueSet = false;
// Apply any default settings to Request.
if ('requestDefaults' in configIn) {
request = request.defaults(configIn.requestDefaults);
}
Object.keys(configIn).forEach(function (key) {
if (isValidConfigKey(key)) {
config[key] = configIn[key];
configValueSet = true;
}
});
if (configIn.facebookAppSecret && configIn.facebookAppId) {
fb.setAccessToken(configIn.facebookAppId + '|' + configIn.facebookAppSecret);
}
if (!configValueSet) {
config = null;
}
};
module.exports.setup = function (cb) {
if (!config || !config.facebookAppSecret || !config.facebookAppId) {
if (process.env.FACEBOOK_APP_SECRET) {
config = config || {};
config.facebookAppSecret = process.env.FACEBOOK_APP_SECRET;
}
if (process.env.FACEBOOK_APP_ID) {
config = config || {};
config.facebookAppId = process.env.FACEBOOK_APP_ID;
}
}
return cb();
};
module.exports.validatePurchase = function (oneTimeAppAccessToken, receipt, cb) {
var appId = config.facebookAppId;
var appSecret = config.facebookAppSecret;
if (oneTimeAppAccessToken) {
verbose.log('<Facebook> Using dynamic app access token:', oneTimeAppAccessToken);
var splitedToken = oneTimeAppAccessToken.split('|');
appId = splitedToken[0];
appSecret = splitedToken[1];
}
// see "Order Fulfillment" to understand what following steps are at https://developers.facebook.com/docs/games_payments/fulfillment#orderfulfillment
verbose.log('<Facebook> Validate signed_request: ', receipt);
var signAndRequest = receipt.split('.');
var encodedSign = signAndRequest[0];
var encodedPurchase = signAndRequest[1];
if (signAndRequest.length !== 2) {
verbose.log('<Facebook> Receipt involves unrelated data');
cb(new Error('failed to validate purchase: involve unrelated data'), {
status: constants.VALIDATION.FAILURE,
message: 'involve unrelated data'
});
return;
}
// because urlbase64.decode executes decoding after triming encodedSign, check whether it involves non-urlbase64 character
// encodedPurchase is not the case because crypto.createHmac makes non-urlbase64 characters into consideration.
if (!urlbase64.validate(encodedSign)) {
verbose.log('<Facebook> Sign is not urlsafe-base64 encoded');
cb(new Error('failed to validate purchase: signature is not a valid urlsafe-base64 form'), {
status: constants.VALIDATION.FAILURE,
message: 'signature is not a valid urlsafe-base64 form'
});
return;
}
var sign = '';
var purchase = '';
var purchaseObj = '';
try {
sign = urlbase64.decode(encodedSign);
purchase = urlbase64.decode(encodedPurchase);
purchaseObj = JSON.parse(purchase);
} catch (e) {
verbose.log('<Facebook> Failed to parse receipt: ', e);
cb(new Error('failed to validate purchase: receipt is malformed'), {
status: constants.VALIDATION.FAILURE,
message: 'receipt is malformed'
});
return;
}
if (purchaseObj.algorithm !== 'HMAC-SHA256') {
verbose.log('<Facebook> Receipt sign algorithm is not HMAC-SHA256');
cb(new Error('failed to validate purchase: invalid algorithm'), {
status: constants.VALIDATION.FAILURE,
message: 'invalid algorithm'
});
return;
}
if (purchaseObj.status !== 'completed') {
verbose.log('<Facebook> Receipt status is not completed');
cb(new Error('failed to validate purchase: payments not completed: ' + purchaseObj.status), {
status: constants.VALIDATION.FAILURE,
message: 'payments not completed: ' + purchaseObj.status
});
return;
}
var hmac = crypto.createHmac('sha256', appSecret).update(encodedPurchase).digest();
if (!sign.equals(hmac)) {
verbose.log('<Facebook> Sign is invalid');
cb(new Error('failed to validate purchase: invalid sign'), {
status: constants.VALIDATION.FAILURE,
message: 'invalid sign'
});
return;
}
var requestParams = {
fields: 'id,user,actions,application,created_time,country,items,payout_foreign_exchange_rate,phone_support_eligible,refundable_amount,tax,tax_country,test',
access_token: appId + '|' + appSecret, // eslint-disable-line camelcase
};
fb.api(purchaseObj.payment_id + '?', 'get', requestParams, function (paymentRes) {
if (!paymentRes || paymentRes.error) {
verbose.log('<Facebook> Failed to call graph api: ', paymentRes);
paymentRes = paymentRes || {};
cb(new Error('failed to validate purchase: graph api call error: ' + JSON.stringify(paymentRes.error)), {
status: constants.VALIDATION.FAILURE,
message: 'graph api call error: ' + JSON.stringify(paymentRes.error)
});
return;
}
var quantityNum = Number(purchaseObj.quantity);
if (purchaseObj.amount !== paymentRes.actions[0].amount ||
purchaseObj.currency !== paymentRes.actions[0].currency ||
purchaseObj.status !== paymentRes.actions[0].status ||
paymentRes.actions[0].type !== 'charge' ||
quantityNum !== paymentRes.items[0].quantity) {
verbose.log('<Facebook> Receipt info does not match with info from facebook');
cb(new Error('failed to validate purchase: payment information not matching'), {
status: constants.VALIDATION.FAILURE,
message: 'payment information not matching'
});
return;
}
purchaseObj.service = constants.SERVICES.FACEBOOK;
purchaseObj.status = constants.VALIDATION.SUCCESS;
verbose.log('<Facebook> Validation success: ', purchaseObj);
cb(null, purchaseObj);
return;
});
};
module.exports.getPurchaseData = function (purchase) {
if (!purchase) {
return null;
}
var data = [];
var purchaseInfo = responseData.parse(purchase);
// purchase(singed_request) already has product_id and quantity
purchaseInfo.transactionId = purchase.payment_id;
purchaseInfo.purchaseDate = purchase.purchase_time * 1000;
data.push(purchaseInfo);
return data;
};