in-app-purchase-iec-develop
Version:
In-App-Purchase validation and subscription management for iOS, Android, Amazon, and Windows
408 lines (374 loc) • 12.7 kB
JavaScript
'use strict';
var async = require('./lib/async');
var apple = require('./lib/apple');
var GoogleModule = require('./lib/google');
var windows = require('./lib/windows');
var amazonManager = require('./lib/amazonManager');
var facebook = require('./lib/facebook');
var roku = require('./lib/roku');
var constants = require('./constants');
var verbose = require('./lib/verbose');
var IS_WINDOWS = '<\/Receipt>';
function IAPInstance() {
this.amazon = null;
this.google = new GoogleModule();
}
IAPInstance.prototype.handlePromisedFunctionCb = function (resolve, reject) {
return function _handlePromisedCallback(error, response) {
if (error) {
var errorData = { error: error, status: null, message: null };
if (response !== null && typeof response === 'object') {
errorData.status = response.status;
errorData.message = response.message;
}
return reject(JSON.stringify(errorData), response);
}
return resolve(response);
};
};
IAPInstance.prototype.config = function (configIn) {
apple.readConfig(configIn);
this.google.readConfig(configIn);
windows.readConfig(configIn);
this.amazon = amazonManager.create(configIn);
facebook.readConfig(configIn);
roku.readConfig(configIn);
verbose.setup(configIn);
};
// services
const SERVICES = {
UNITY: 'unity',
APPLE: 'apple',
GOOGLE: 'google',
WINDOWS: 'windows',
AMAZON: 'amazon',
ROKU: 'roku',
FACEBOOK: 'facebook'
};
const UNITY = {
APPLE: 'AppleAppStore',
GOOGLE: 'GooglePlay',
AMAZON: 'AmazonApps'
};
// validation
const VALIDATION = {
SUCCESS: 0,
FAILURE: 1,
POSSIBLE_HACK: 2
};
IAPInstance.prototype.setup = function (cb) {
if (!cb && Promise) {
return new Promise((resolve, reject) => {
this.setup(this.handlePromisedFunctionCb(resolve, reject));
});
}
async.series([
(next) => apple.setup(next),
(next) => this.google.setup(next),
(next) => this.amazon.setup(next),
(next) => facebook.setup(next),
], cb);
};
IAPInstance.prototype.getService = function (receipt) {
if (!receipt) {
throw new Error('Receipt was null or undefined');
}
if (receipt.indexOf && receipt.indexOf(IS_WINDOWS) !== -1) {
return SERVICES.WINDOWS;
}
if (typeof receipt === 'object') {
// receipt could be either Google, Amazon, or Unity (Apple or Google or Amazon)
if (isUnityReceipt(receipt)) {
return SERVICES.UNITY;
}
if (receipt.signature) {
return SERVICES.GOOGLE;
} else if (receipt.purchaseToken) {
return SERVICES.GOOGLE;
} else {
return SERVICES.AMAZON;
}
}
if (typeof receipt === 'string') {
var characters = receipt.match(/\w/g) || '';
var dashes = receipt.match(/-/g) || '';
if (characters.length === 32 && dashes.length === 4) {
return SERVICES.ROKU;
}
}
try {
// receipt could be either Google, Amazon, or Unity (Apple or Google or Amazon)
var parsed = JSON.parse(receipt);
if (isUnityReceipt(parsed)) {
return SERVICES.UNITY;
}
if (parsed.signature) {
return SERVICES.GOOGLE;
} else if (receipt.purchaseToken) {
return SERVICES.GOOGLE;
} else {
return SERVICES.AMAZON;
}
} catch (error) {
var dotSplitedReceipt = receipt.split('.');
if (dotSplitedReceipt.length === 2) {
return SERVICES.FACEBOOK;
}
return SERVICES.APPLE;
}
};
IAPInstance.prototype.validate = function (service, receipt, cb) {
if (receipt === undefined && cb === undefined) {
// we are given 1 argument as: const promise = .validate(receipt)
receipt = service;
service = this.getService(receipt);
console.log(service);
}
if (cb === undefined && typeof receipt === 'function') {
// we are given 2 arguments as: .validate(receipt, cb)
cb = receipt;
receipt = service;
service = this.getService(receipt);
}
if (!cb && Promise) {
return new Promise((resolve, reject) => {
this.validate(
service,
receipt,
this.handlePromisedFunctionCb(resolve, reject)
);
});
}
if (service === SERVICES.UNITY) {
service = getServiceFromUnityReceipt(receipt);
receipt = parseUnityReceipt(receipt);
}
if (!service) {
return cb(new Error('invalid service given: null'));
}
switch (service) {
case SERVICES.APPLE:
apple.validatePurchase(null, receipt, cb);
break;
case SERVICES.GOOGLE:
this.google.validatePurchase(null, receipt, cb);
break;
case SERVICES.WINDOWS:
windows.validatePurchase(receipt, cb);
break;
case SERVICES.AMAZON:
this.amazon.validatePurchase(null, receipt, cb);
break;
case SERVICES.FACEBOOK:
facebook.validatePurchase(null, receipt, cb);
break;
case SERVICES.ROKU:
roku.validatePurchase(null, receipt, cb);
break;
default:
return cb(new Error('invalid service given: ' + service));
}
};
IAPInstance.prototype.validateOnce = function (service, secretOrPubKey, receipt, cb) {
if (receipt === undefined && cb === undefined) {
// we are given 2 arguments as: const promise = .validateOnce(receipt, secretOrPubKey)
receipt = service;
service = this.getService(receipt);
}
if (cb === undefined && typeof receipt === 'function') {
// we are given 3 arguments as: .validateOnce(receipt, secretPubKey, cb)
cb = receipt;
receipt = service;
service = this.getService(receipt);
}
if (!cb && Promise) {
return new Promise((resolve, reject) => {
this.validateOnce(
service,
secretOrPubKey,
receipt,
this.handlePromisedFunctionCb(resolve, reject)
);
});
}
if (service === SERVICES.UNITY) {
service = getServiceFromUnityReceipt(receipt);
receipt = parseUnityReceipt(receipt);
}
if (!secretOrPubKey && service !== SERVICES.APPLE && service !== SERVICES.WINDOWS) {
verbose.log('<.validateOnce>', service, receipt);
return cb(new Error('missing secret or public key for dynamic validation:' + service));
}
switch (service) {
case SERVICES.APPLE:
apple.validatePurchase(secretOrPubKey, receipt, cb);
break;
case SERVICES.GOOGLE:
this.google.validatePurchase(secretOrPubKey, receipt, cb);
break;
case SERVICES.WINDOWS:
windows.validatePurchase(receipt, cb);
break;
case SERVICES.AMAZON:
this.amazon.validatePurchase(secretOrPubKey, receipt, cb);
break;
case SERVICES.FACEBOOK:
facebook.validatePurchase(secretOrPubKey, receipt, cb);
break;
case SERVICES.ROKU:
roku.validatePurchase(secretOrPubKey, receipt, cb);
break;
default:
verbose.log('<.validateOnce>', secretOrPubKey, receipt);
return cb(new Error('invalid service given: ' + service));
}
};
IAPInstance.prototype.isValidated = function (response) {
if (response && response.status === constants.VALIDATION.SUCCESS) {
return true;
}
return false;
};
IAPInstance.prototype.isExpired = function (purchasedItem) {
if (!purchasedItem || !purchasedItem.transactionId) {
throw new Error('invalid purchased item given:\n' + JSON.stringify(purchasedItem));
}
if (purchasedItem.cancellationDate) {
// it has been cancelled
return true;
}
if (!purchasedItem.expirationDate) {
// there is no expiration date with this item
return false;
}
if (Date.now() - purchasedItem.expirationDate >= 0) {
return true;
}
// has not expired yet
return false;
};
IAPInstance.prototype.isCanceled = function (purchasedItem) {
if (!purchasedItem || !purchasedItem.transactionId) {
throw new Error('invalid purchased item given:\n' + JSON.stringify(purchasedItem));
}
if (purchasedItem.cancellationDate) {
// it has been cancelled
return true;
}
return false;
};
IAPInstance.prototype.getPurchaseData = function (purchaseData, options) {
if (!purchaseData || !purchaseData.service) {
return null;
}
switch (purchaseData.service) {
case SERVICES.APPLE:
return apple.getPurchaseData(purchaseData, options);
case SERVICES.GOOGLE:
return this.google.getPurchaseData(purchaseData, options);
case SERVICES.WINDOWS:
return windows.getPurchaseData(purchaseData, options);
case SERVICES.AMAZON:
return this.amazon.getPurchaseData(purchaseData, options);
case SERVICES.FACEBOOK:
return facebook.getPurchaseData(purchaseData, options);
case SERVICES.ROKU:
return roku.getPurchaseData(purchaseData, options);
default:
return null;
}
};
IAPInstance.prototype.refreshGoogleToken = function (cb) {
if (!cb && Promise) {
return new Promise((resolve, reject) => {
this.refreshGoogleToken(this.handlePromisedFunctionCb(resolve, reject));
});
}
this.google.refreshToken(cb);
};
IAPInstance.prototype.setAmazonValidationHost = function (vhost) {
if (this.amazon.setValidationHost) {
return this.amazon.setValidationHost(vhost);
}
return false;
};
IAPInstance.prototype.resetAmazonValidationHost = function () {
if (this.amazon.resetValidationHost) {
return this.amazon.resetValidationHost();
}
return false;
};
function isUnityReceipt(receipt) {
if (receipt.Store) {
if (
receipt.Store === constants.UNITY.GOOGLE ||
receipt.Store === constants.UNITY.APPLE ||
receipt.Store === constants.UNITY.AMAZON
) {
return true;
}
}
return false;
}
function getServiceFromUnityReceipt(receipt) {
if (typeof receipt !== 'object') {
// at this point we have already established the fact that receipt is a valid JSON string
receipt = JSON.parse(receipt);
}
switch (receipt.Store) {
case constants.UNITY.GOOGLE:
return SERVICES.GOOGLE;
case constants.UNITY.APPLE:
return SERVICES.APPLE;
case constants.UNITY.AMAZON:
return SERVICES.AMAZON;
}
// invalid Store value
return null;
}
function parseUnityReceipt(receipt) {
verbose.log('Parse Unity receipt as ' + receipt.Store);
if (typeof receipt !== 'object') {
// at this point we have already established the fact that receipt is a valid JSON string
receipt = JSON.parse(receipt);
}
switch (receipt.Store) {
case constants.UNITY.GOOGLE:
if (typeof receipt.Payload === 'string') {
try {
receipt.Payload = JSON.parse(receipt.Payload);
} catch (error) {
throw error;
}
}
var payloadContent = typeof receipt.Payload.json !== 'object' ? JSON.parse(receipt.Payload.json) : receipt.Payload.json;
return {
data: receipt.Payload.json,
signature: receipt.Payload.signature,
// add field necessary to use google service account
packageName: payloadContent.packageName,
productId: payloadContent.productId,
purchaseToken: payloadContent.purchaseToken,
subscription: (receipt.Subscription !== undefined && receipt.Subscription)
};
case constants.UNITY.AMAZON:
if (typeof receipt.Payload === 'string') {
try {
receipt.Payload = JSON.parse(receipt.Payload);
} catch (error) {
throw error;
}
}
return receipt.Payload;
case constants.UNITY.APPLE:
return receipt.Payload;
}
}
// test use only
module.exports.reset = function () {
// resets google setup
this.google.reset();
};
module.exports = function () {
return new IAPInstance();
};