in-app-purchase-multiple-iec
Version:
In-App-Purchase validation and subscription management for iOS, Android, Amazon, and Windows
442 lines (409 loc) • 13.4 kB
JavaScript
"use strict";
var async = require("./lib/async");
var AppleModule = require("./lib/apple");
var GoogleModule = require("./lib/google");
var windows = require("./lib/windows");
var amazonManager = require("./lib/amazonManager");
var FacebookModule = 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();
this.apple = new AppleModule();
this.facebook = new FacebookModule();
}
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) {
this.apple.readConfig(configIn);
this.google.readConfig(configIn);
windows.readConfig(configIn);
this.amazon = amazonManager.create(configIn);
this.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) => this.apple.setup(next),
(next) => this.google.setup(next),
(next) => this.amazon.setup(next),
(next) => this.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:
this.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:
this.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:
this.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:
this.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 this.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 this.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();
};
IAPInstance.prototype.switchEnv = function (isSanbox) {
// update apple config
this.apple.switchEnv(isSanbox);
};
IAPInstance.prototype.getEnvApple = function () {
return this.apple.getEnvApple();
};
module.exports = function () {
return new IAPInstance();
};