UNPKG

cc.fovea.cordova.purchase

Version:

Cordova Purchase plugin for iOS, Android, Windows (AppStore, Play, UWP)

522 lines (459 loc) 18.1 kB
/* * A plugin to enable iOS In-App Purchases. * * Copyright (c) Matt Kane 2011 * Copyright (c) Guillaume Charhon 2012 * Copyright (c) Jean-Christophe Hoelt 2013 */ /*eslint camelcase:0 */ /*global cordova, window */ (function(){ var noop = function () {}; var log = noop; var exec = function (methodName, options, success, error) { cordova.exec(success, error, "InAppPurchase", methodName, options); }; var protectCall = function (callback, context) { if (!callback) { return; } try { var args = Array.prototype.slice.call(arguments, 2); callback.apply(this, args); } catch (err) { log('exception in ' + context + ': "' + err + '"'); } }; var InAppPurchase = function () { this.options = {}; this.transactionForProduct = {}; if (window.localStorage && window.localStorage.sk_transactionForProduct) this.transactionForProduct = JSON.parse(window.localStorage.sk_transactionForProduct); // Remove support for receipt.forTransaction(...) // `appStoreReceipt` is now the only supported receipt format on iOS (drops support for iOS <= 6) if (window.localStorage.sk_receiptForTransaction) delete window.localStorage.sk_receiptForTransaction; }; var initialized = false; InAppPurchase.prototype.init = function (options, success, error) { this.options = { error: options.error || noop, ready: options.ready || noop, purchase: options.purchase || noop, purchaseEnqueued: options.purchaseEnqueued || noop, purchasing: options.purchasing || noop, deferred: options.deferred || noop, finish: options.finish || noop, restore: options.restore || noop, receiptsRefreshed: options.receiptsRefreshed || noop, restoreFailed: options.restoreFailed || noop, restoreCompleted: options.restoreCompleted || noop, downloadActive: options.downloadActive || noop, downloadCancelled: options.downloadCancelled || noop, downloadFailed: options.downloadFailed || noop, downloadFinished: options.downloadFinished || noop, downloadPaused: options.downloadPaused || noop, downloadWaiting: options.downloadWaiting || noop }; if (options.debug) { exec('debug', [], noop, noop); log = function (msg) { console.log("InAppPurchase[js]: " + msg); }; } if (options.autoFinish) { exec('autoFinish', [], noop, noop); } if (options.disableHostedContent) { exec('disableHostedContent', [], noop, noop); } var that = this; var setupOk = function () { log('setup ok'); protectCall(that.options.ready, 'options.ready'); protectCall(success, 'init.success'); initialized = true; that.processPendingUpdates(); // Is there a reason why we wouldn't like to do this automatically? // YES! it does ask the user for his password. // that.restore(); }; var setupFailed = function () { log('setup failed'); protectCall(options.error, 'options.error', store.ERR_SETUP, 'Setup failed'); protectCall(error, 'init.error'); }; exec('setup', [], setupOk, setupFailed); }; /* * Makes an in-app purchase. * * @param {String} productId The product identifier. e.g. "com.example.MyApp.myproduct" * @param {int} quantity Quantity of product to purchase */ InAppPurchase.prototype.purchase = function (productId, quantity, applicationUsername, discount) { quantity = quantity | 0 || 1; var options = this.options; // Many people forget to load information about their products from apple's servers before allowing // users to purchase them... leading them to spam us with useless issues and comments. // Let's chase them down! if (!InAppPurchase._productIds || InAppPurchase._productIds.indexOf(productId) < 0) { var msg = 'Purchasing ' + productId + ' failed. Ensure the product was loaded first with storekit.load(...)!'; log(msg); if (typeof options.error === 'function') { protectCall(options.error, 'options.error', store.ERR_PURCHASE, 'Trying to purchase a unknown product.', productId, quantity); } return; } var purchaseOk = function () { log('Purchased ' + productId); if (typeof options.purchaseEnqueued === 'function') { protectCall(options.purchaseEnqueued, 'options.purchaseEnqueued', productId, quantity); } }; var purchaseFailed = function () { var errmsg = 'Purchasing ' + productId + ' failed'; log(errmsg); if (typeof options.error === 'function') { protectCall(options.error, 'options.error', store.ERR_PURCHASE, errmsg, productId, quantity); } }; exec('purchase', [productId, quantity, applicationUsername, discount || {}], purchaseOk, purchaseFailed); }; /* * Checks if device/user is allowed to make in-app purchases */ InAppPurchase.prototype.canMakePayments = function(success, error){ return exec("canMakePayments", [], success, error); }; /* * Asks the payment queue to restore previously completed purchases. * The restored transactions are passed to the onRestored callback, so make sure you define a handler for that first. * */ InAppPurchase.prototype.restore = function() { this.needRestoreNotification = true; exec('restoreCompletedTransactions', []); }; InAppPurchase.prototype.manageSubscriptions = function () { exec('manageSubscriptions', []); }; InAppPurchase.prototype.manageBilling = function () { exec('manageBilling', []); }; InAppPurchase.prototype.presentCodeRedemptionSheet = function () { exec('presentCodeRedemptionSheet', []); }; /* * Requests all active downloads be paused */ InAppPurchase.prototype.pause = function() { var ok = function() { log("Paused active downloads"); if (typeof this.options.paused === "function") { protectCall(this.options.paused, "options.paused"); } }; var failed = function() { var errmsg = "Pausing active downloads failed"; log(errmsg); if (typeof this.options.error === "function") { protectCall(this.options.error, "options.error", store.ERR_DOWNLOAD, errmsg); } }; return exec('pause', [], ok, failed); }; /* * Requests all paused active downloads be resumed */ InAppPurchase.prototype.resume = function() { var ok = function() { log("Resumed active downloads"); if (typeof this.options.resumed === "function") { protectCall(this.options.resumed, "options.resumed"); } }; var failed = function() { var errmsg = "Resuming active downloads failed"; log(errmsg); if (typeof this.options.error === "function") { protectCall(this.options.error, "options.error", store.ERR_DOWNLOAD, errmsg); } }; return exec('resume', [], ok, failed); }; /* * Requests all active downloads be cancelled */ InAppPurchase.prototype.cancel = function() { var ok = function() { log("Cancelled active downloads"); if (typeof this.options.cancelled === "function") { protectCall(this.options.cancelled, "options.cancelled"); } }; var failed = function() { var errmsg = "Cancelling active downloads failed"; log(errmsg); if (typeof this.options.error === "function") { protectCall(this.options.error, "options.error", store.ERR_DOWNLOAD, errmsg); } }; return exec('cancel', [], ok, failed); }; /* * Retrieves localized product data, including price (as localized * string), name, description of multiple products. * * @param {Array} productIds * An array of product identifier strings. * * @param {Function} callback * Called once with the result of the products request. Signature: * * function(validProducts, invalidProductIds) * * where validProducts receives an array of objects of the form: * * { * id: "<productId>", * title: "<localised title>", * description: "<localised escription>", * price: "<localised price>" * } * * and invalidProductIds receives an array of product identifier * strings which were rejected by the app store. */ InAppPurchase.prototype.load = function (productIds, success, error) { var options = this.options; if (typeof productIds === "string") { productIds = [productIds]; } if (!productIds) { // Empty array, nothing to do. protectCall(success, 'load.success', [], []); } else if (!productIds.length) { // Empty array, nothing to do. protectCall(success, 'load.success', [], []); } else { if (typeof productIds[0] !== 'string') { var msg = 'invalid productIds given to store.load: ' + JSON.stringify(productIds); log(msg); protectCall(options.error, 'options.error', store.ERR_LOAD, msg); protectCall(error, 'load.error', store.ERR_LOAD, msg); return; } log('load ' + JSON.stringify(productIds)); var loadOk = function (array) { var valid = array[0]; var invalid = array[1]; log('load ok: { valid:' + JSON.stringify(valid) + ' invalid:' + JSON.stringify(invalid) + ' }'); protectCall(success, 'load.success', valid, invalid); }; var loadFailed = function (errMessage) { log('load failed'); log(errMessage); var message = 'Load failed: ' + errMessage; protectCall(options.error, 'options.error', store.ERR_LOAD, message); protectCall(error, 'load.error', store.ERR_LOAD, message); }; InAppPurchase._productIds = productIds; exec('load', [productIds], loadOk, loadFailed); } }; /* * Finish an unfinished transaction. * * @param {String} transactionId * Identifier of the transaction to finish. * * You have to call this method manually except when using the autoFinish option. */ InAppPurchase.prototype.finish = function (transactionId) { exec('finishTransaction', [transactionId], noop, noop); }; var pendingUpdates = [], pendingDownloadUpdates = []; InAppPurchase.prototype.processPendingUpdates = function() { for (var i = 0; i < pendingUpdates.length; ++i) { this.updatedTransactionCallback.apply(this, pendingUpdates[i]); } pendingUpdates = []; for (var j = 0; j < pendingDownloadUpdates.length; ++j) { this.updatedDownloadCallback.apply(this, pendingDownloadUpdates[j]); } pendingDownloadUpdates = []; }; // This is called from native. // // Note that it may eventually be called before initialization... unfortunately. // In this case, we'll just keep pending updates in a list for later processing. InAppPurchase.prototype.updatedTransactionCallback = function (state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt, originalTransactionIdentifier) { if (!initialized) { var args = Array.prototype.slice.call(arguments); pendingUpdates.push(args); return; } if (productId && transactionIdentifier) { log("product " + productId + " has a transaction in progress: " + transactionIdentifier); this.transactionForProduct[productId] = transactionIdentifier; } switch(state) { case "PaymentTransactionStatePurchasing": protectCall(this.options.purchasing, 'options.purchasing', productId); return; case "PaymentTransactionStatePurchased": protectCall(this.options.purchase, 'options.purchase', transactionIdentifier, productId, originalTransactionIdentifier); return; case "PaymentTransactionStateDeferred": protectCall(this.options.deferred, 'options.deferred', productId); return; case "PaymentTransactionStateFailed": protectCall(this.options.error, 'options.error', errorCode, errorText, { productId: productId }); return; case "PaymentTransactionStateRestored": protectCall(this.options.restore, 'options.restore', transactionIdentifier, productId); return; case "PaymentTransactionStateFinished": protectCall(this.options.finish, 'options.finish', transactionIdentifier, productId); return; } }; InAppPurchase.prototype.updatedDownloadCallback = function(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt, progress, timeRemaining) { if (!initialized) { var args = Array.prototype.slice.call(arguments); pendingDownloadUpdates.push(args); return; } switch (state) { case "DownloadStateActive": protectCall(this.options.downloadActive, "options.downloadActive", transactionIdentifier, productId, progress, timeRemaining); return; case "DownloadStateCancelled": protectCall(this.options.downloadCancelled, "options.downloadCancelled", transactionIdentifier, productId); return; case "DownloadStateFailed": protectCall(this.options.downloadFailed, "options.downloadFailed", transactionIdentifier, productId, errorCode, errorText); return; case "DownloadStateFinished": protectCall(this.options.downloadFinished, "options.downloadFinished", transactionIdentifier, productId); return; case "DownloadStatePaused": protectCall(this.options.downloadPaused, "options.downloadPaused", transactionIdentifier, productId); return; case "DownloadStateWaiting": protectCall(this.options.downloadWaiting, "options.downloadWaiting", transactionIdentifier, productId); return; } }; InAppPurchase.prototype.restoreCompletedTransactionsFinished = function () { if (this.needRestoreNotification) delete this.needRestoreNotification; else return; protectCall(this.options.restoreCompleted, 'options.restoreCompleted'); }; InAppPurchase.prototype.restoreCompletedTransactionsFailed = function (errorCode) { if (this.needRestoreNotification) delete this.needRestoreNotification; else return; protectCall(this.options.restoreFailed, 'options.restoreFailed', errorCode); }; function parseReceiptArgs(args) { var base64 = args[0]; var bundleIdentifier = args[1]; var bundleShortVersion = args[2]; var bundleNumericVersion = args[3]; var bundleSignature = args[4]; log('infoPlist: ' + bundleIdentifier + "," + bundleShortVersion + "," + bundleNumericVersion + "," + bundleSignature); return { appStoreReceipt: base64, bundleIdentifier: bundleIdentifier, bundleShortVersion: bundleShortVersion, bundleNumericVersion: bundleNumericVersion, bundleSignature: bundleSignature }; } InAppPurchase.prototype.refreshReceipts = function(successCb, errorCb) { var that = this; var loaded = function (args) { var data = parseReceiptArgs(args); that.appStoreReceipt = data.appStoreReceipt; protectCall(that.options.receiptsRefreshed, 'options.receiptsRefreshed', data); protectCall(successCb, "refreshReceipts.success", data); }; var error = function(errMessage) { log('refresh receipt failed: ' + errMessage); protectCall(that.options.error, 'options.error', store.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); protectCall(errorCb, "refreshReceipts.error", store.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); }; this.appStoreReceipt = null; log('refreshing appStoreReceipt'); exec('appStoreRefreshReceipt', [], loaded, error); }; InAppPurchase.prototype.loadReceipts = function (callback, errorCb) { var that = this; var data; var loaded = function (args) { data = parseReceiptArgs(args); that.appStoreReceipt = data.appStoreReceipt; callCallback(); }; var error = function (errMessage) { log('load failed: ' + errMessage); protectCall(that.options.error, 'options.error', store.ERR_LOAD_RECEIPTS, 'Failed to load receipt: ' + errMessage); protectCall(errorCb, 'loadReceipts.error', store.ERR_LOAD_RECEIPTS, 'Failed to load receipt: ' + errMessage); }; function callCallback() { protectCall(callback, 'loadReceipts.callback', data); } log('loading appStoreReceipt'); exec('appStoreReceipt', [], loaded, error); }; /* * This queue stuff is here because we may be sent events before listeners have been registered. This is because if we have * incomplete transactions when we quit, the app will try to run these when we resume. If we don't register to receive these * right away then they may be missed. As soon as a callback has been registered then it will be sent any events waiting * in the queue. */ InAppPurchase.prototype.runQueue = function () { if(!this.eventQueue.length || !this.onPurchased && !this.onFailed && !this.onRestored) { return; } var args; /* We can't work directly on the queue, because we're pushing new elements onto it */ var queue = this.eventQueue.slice(); this.eventQueue = []; args = queue.shift(); while (args) { this.updatedTransactionCallback.apply(this, args); args = queue.shift(); } if (!this.eventQueue.length) { this.unWatchQueue(); } }; InAppPurchase.prototype.watchQueue = function () { if (this.timer) { return; } this.timer = window.setInterval(function () { window.storekit.runQueue(); }, 10000); }; InAppPurchase.prototype.unWatchQueue = function () { if (this.timer) { window.clearInterval(this.timer); this.timer = null; } }; InAppPurchase.prototype.eventQueue = []; InAppPurchase.prototype.timer = null; window.storekit = new InAppPurchase(); })();