From f31fc68169c098dacec9ded0bfc393c267d51911 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Thu, 30 Oct 2014 23:50:49 +0200 Subject: [PATCH 01/11] Add ESLint + Fix raised issues --- .eslintrc | 18 + .jshintrc | 9 + Makefile | 12 +- doc/api.md | 16 +- package.json | 1 + src/js/constants.js | 2 +- src/js/error-callbacks.js | 4 +- src/js/error.js | 4 +- src/js/get.js | 2 +- src/js/log.js | 2 +- src/js/off.js | 3 +- src/js/once.js | 4 +- src/js/order.js | 9 +- src/js/platforms/android-adapter.js | 14 +- src/js/platforms/android-bridge.js | 52 +-- src/js/platforms/ios-adapter.js | 45 +- src/js/platforms/ios-bridge.js | 52 ++- src/js/product-internal.js | 2 +- src/js/product.js | 26 +- src/js/products.js | 2 +- src/js/queries.js | 16 +- src/js/ready.js | 2 +- src/js/refresh.js | 6 +- src/js/register.js | 7 +- src/js/store-ios.js | 2 +- src/js/store.js | 36 +- src/js/trigger.js | 2 +- src/js/utils.js | 8 +- src/js/validator.js | 10 +- src/js/when.js | 14 +- test/js/helper.js | 36 +- test/js/run.js | 47 +- test/js/test-android.js | 8 +- test/js/test-error.js | 3 + test/js/test-finish.js | 5 + test/js/test-ios.js | 7 +- test/js/test-off.js | 4 +- test/js/test-order.js | 3 + test/js/test-queries.js | 55 ++- test/js/test-ready.js | 3 + test/js/test-register.js | 8 +- test/js/test-utils.js | 23 +- test/js/test-verify.js | 21 +- test/js/test-when.js | 30 +- www/store-android.js | 116 ++--- www/store-ios.js | 655 ++++++++++++++-------------- 46 files changed, 754 insertions(+), 652 deletions(-) create mode 100644 .eslintrc create mode 100644 .jshintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..6b3a2d2f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "env" : { + "node" : true, + "browser" : true + }, + "rules" : { + "strict": 1, + "quotes": 0, + "no-underscore-dangle": 0, + "no-multi-spaces": 0, + "key-spacing": 0, + "curly": 0, + "no-use-before-define": [ 1, "nofunc" ] + }, + "globals" : { + "store": true + } +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..c18c257f --- /dev/null +++ b/.jshintrc @@ -0,0 +1,9 @@ +{ + "node": true, + "browser": true, + "undef": true, + "globals": { + "store": true, + "cordova": true + } +} diff --git a/Makefile b/Makefile index dc4b3640..19894f59 100644 --- a/Makefile +++ b/Makefile @@ -33,15 +33,19 @@ prepare-test-js: @cp src/js/platforms/*-adapter.js test/tmp/ @#node_modules/.bin/istanbul instrument --no-compact --output test/tmp/store-test.js test/store-test-src.js -jshint: check-jshint +jshint: check-jshint sync-android @echo "- JSHint" - @node_modules/.bin/jshint src/js/*.js test/js/*.js + @node_modules/.bin/jshint --config .jshintrc src/js/*.js src/js/platforms/*.js test/js/*.js -test-js: jshint prepare-test-js +eslint: jshint + @echo "- ESLint" + @node_modules/.bin/eslint --config .eslintrc src/js/*.js src/js/platforms/*.js test/js/*.js + +test-js: jshint eslint prepare-test-js @echo "- Mocha" @node_modules/.bin/istanbul test --root test/tmp test/js/run.js -test-js-coverage: jshint prepare-test-js +test-js-coverage: jshint eslint prepare-test-js @echo "- Mocha / Instanbul" @node_modules/.bin/istanbul cover --root test/ test/js/run.js @node_modules/.bin/coveralls < coverage/lcov.info diff --git a/doc/api.md b/doc/api.md index 6f22a24e..1969c9a3 100644 --- a/doc/api.md +++ b/doc/api.md @@ -61,7 +61,7 @@ be contacted to load informations about the registered products: human readable `title` and `description`, `price`, etc. This isn't an optional step as some despotic store owners (like Apple) require you -to display information about a product as retrieved from their server: no +to display information about a product as retrieved from their server: no hard-coding of price and title allowed! This is also convenient for you as you can change the price of your items knowing that it'll be reflected instantly on your clients' devices. @@ -80,9 +80,9 @@ Let's demonstrate this with an example: render(); store.when("cc.fovea.test1").updated(render); } - + function render() { - + // Get the product from the pool. var product = store.get("cc.fovea.test1"); @@ -102,13 +102,13 @@ Let's demonstrate this with an example: + "
" + product.description + "
" + "
" + product.price + "
" ); - + // Is this product owned? Give him a special class. if (product.owned) $el.addClass("owned"); else $el.removeClass("owned"); - + // Is an order for this product in progress? Can't be ordered right now? if (product.canPurchase) $el.addClass("can-purchase"); @@ -116,7 +116,7 @@ Let's demonstrate this with an example: $el.removeClass("can-purchase"); } } - + // method called when the view is hidden function hide() { // stop monitoring the product @@ -387,7 +387,7 @@ A Promise with the following methods: - `error(function(err){})` - validation failed, either because of expiry or communication failure. - - `err` is a [store.Error object](#errors), with a code expected to be + - `err` is a [store.Error object](#errors), with a code expected to be `store.ERR_PAYMENT_EXPIRED` or `store.ERR_VERIFICATION_FAILED`. @@ -904,7 +904,7 @@ Only supports JSON requests. Options: -* `url`: +* `url`: * `method`: HTTP method to use (GET, POST, ...) * `success`: callback(data) * `error`: callback(statusCode, statusText) diff --git a/package.json b/package.json index d1a6556b..fe74f359 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "homepage": "https://github.com/j3k0/PhoneGap-InAppPurchase-iOS", "devDependencies": { "cordova": "^3.6.3-0.2.13", + "eslint": "^0.9.1", "istanbul": "^0.3.2", "jshint": "^2.5.6", "mocha": "^1.21.4", diff --git a/src/js/constants.js b/src/js/constants.js index 5ba39b33..789b41cd 100644 --- a/src/js/constants.js +++ b/src/js/constants.js @@ -69,4 +69,4 @@ var ERROR_CODES_BASE = 6777000; /*///*/ store.CONNECTION_FAILED = 6778002; /*///*/ store.PURCHASE_EXPIRED = 6778003; -}).call(this); +})(); diff --git a/src/js/error-callbacks.js b/src/js/error-callbacks.js index bcc478ab..801c4e70 100644 --- a/src/js/error-callbacks.js +++ b/src/js/error-callbacks.js @@ -1,7 +1,7 @@ (function(){ 'use strict'; -/// +/// /// ## *store.error.callbacks* array /// /// Array of user registered error callbacks. @@ -47,4 +47,4 @@ function deferThrow(err) { setTimeout(function() { throw err; }, 1); } -}).call(this); +})(); diff --git a/src/js/error.js b/src/js/error.js index 49680cbe..312e7ff0 100644 --- a/src/js/error.js +++ b/src/js/error.js @@ -3,7 +3,7 @@ /// /// ## *store.Error* object -/// +/// /// All error callbacks takes an `error` object as parameter. store.Error = function(options) { @@ -80,4 +80,4 @@ store.error.unregister = function(cb) { store.error.callbacks.unregister(cb); }; -}).call(this); +})(); diff --git a/src/js/get.js b/src/js/get.js index 0abffa83..de6b9bfa 100644 --- a/src/js/get.js +++ b/src/js/get.js @@ -15,4 +15,4 @@ store.get = function(id) { return product; }; -}).call(this); +})(); diff --git a/src/js/log.js b/src/js/log.js index 98f7dc62..fab33d7d 100644 --- a/src/js/log.js +++ b/src/js/log.js @@ -41,4 +41,4 @@ store.log = { debug: function(o) { log(store.DEBUG, o); } }; -}).call(this); +})(); diff --git a/src/js/off.js b/src/js/off.js index bbdbd138..eb06eec9 100644 --- a/src/js/off.js +++ b/src/js/off.js @@ -34,5 +34,4 @@ store.off = function(callback) { store.error.unregister(callback); }; -}).call(this); - +})(); diff --git a/src/js/once.js b/src/js/once.js index 2d615bb4..49bc0658 100644 --- a/src/js/once.js +++ b/src/js/once.js @@ -2,7 +2,7 @@ "use strict"; /// ## *store.once(query)* -/// +/// /// Identical to [`store.when`](#when), but the callback will be called only once. /// After being called, the callback will be unregistered. store.once = function(query, action, callback) { @@ -25,4 +25,4 @@ store.once = function(query, action, callback) { store.once.unregister = store.when.unregister; -}).call(this); +})(); diff --git a/src/js/order.js b/src/js/order.js index 9ac59764..6c56d44b 100644 --- a/src/js/order.js +++ b/src/js/order.js @@ -13,7 +13,7 @@ var callbackId = 0; /// Initiate the purchase of a product. /// /// The `product` argument can be either: -/// +/// /// - the `store.Product` object /// - the product `id` /// - the product `alias` @@ -23,9 +23,8 @@ var callbackId = 0; /// store.order = function(pid) { - var that = this; var p = pid; - + if (typeof pid === "string") { p = store.products.byId[pid] || store.products.byAlias[pid]; if (!p) { @@ -75,7 +74,7 @@ store.order = function(pid) { if (!localCallback.error) return; done(); - cb(p); + cb(err); }); return this; } @@ -97,4 +96,4 @@ store.order.unregister = function(cb) { } }; -}).call(this); +})(); diff --git a/src/js/platforms/android-adapter.js b/src/js/platforms/android-adapter.js index 3487752d..5fea6f25 100644 --- a/src/js/platforms/android-adapter.js +++ b/src/js/platforms/android-adapter.js @@ -1,6 +1,9 @@ (function() { "use strict"; +var initialized = false; +var skus = []; + store.when("refreshed", function() { if (!initialized) init(); }); @@ -9,10 +12,7 @@ store.when("re-refreshed", function() { iabGetPurchases(); }); -var initialized = false; -var skus = []; - -var init = function () { +function init() { if (initialized) return; initialized = true; @@ -30,7 +30,7 @@ var init = function () { showLog: store.verbosity >= store.DEBUG ? true : false }, skus); -}; +} function iabReady() { store.log.debug("android -> ready"); @@ -225,7 +225,7 @@ store.when("product", "finished", function(product) { function(err, code) { // error // can't finish. store.error({ - code: code || ERR_UNKNOWN, + code: code || store.ERR_UNKNOWN, message: err }); }, @@ -236,4 +236,4 @@ store.when("product", "finished", function(product) { } }); -}).call(this); +})(); diff --git a/src/js/platforms/android-bridge.js b/src/js/platforms/android-bridge.js index f010fdcf..1d18d4aa 100644 --- a/src/js/platforms/android-bridge.js +++ b/src/js/platforms/android-bridge.js @@ -2,6 +2,7 @@ * Copyright (C) 2012-2013 by Guillaume Charhon * Modifications 10/16/2013 by Brian Thurlow */ +/*global cordova */ (function() { "use strict"; @@ -21,38 +22,39 @@ InAppBilling.prototype.init = function (success, fail, options, skus) { this.options = { showLog: options.showLog !== false }; - + if (this.options.showLog) { log('setup ok'); } - + var hasSKUs = false; //Optional Load SKUs to Inventory. if(typeof skus !== "undefined"){ if (typeof skus === "string") { - skus = [skus]; - } - if (skus.length > 0) { - if (typeof skus[0] !== 'string') { - var msg = 'invalid productIds: ' + JSON.stringify(skus); - if (this.options.showLog) { - log(msg); - } + skus = [skus]; + } + if (skus.length > 0) { + if (typeof skus[0] !== 'string') { + var msg = 'invalid productIds: ' + JSON.stringify(skus); + if (this.options.showLog) { + log(msg); + } fail(msg, store.ERR_INVALID_PRODUCT_ID); - return; - } - if (this.options.showLog) { - log('load ' + JSON.stringify(skus)); - } + return; + } + if (this.options.showLog) { + log('load ' + JSON.stringify(skus)); + } hasSKUs = true; - } + } } - - if(hasSKUs){ - return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", [skus]); - }else { + + if (hasSKUs) { + cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", [skus]); + } + else { //No SKUs - return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", []); + cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", []); } }; InAppBilling.prototype.getPurchases = function (success, fail) { @@ -89,7 +91,7 @@ InAppBilling.prototype.getProductDetails = function (success, fail, skus) { if (this.options.showLog) { log('getProductDetails called!'); } - + if (typeof skus === "string") { skus = [skus]; } @@ -104,9 +106,9 @@ InAppBilling.prototype.getProductDetails = function (success, fail, skus) { return; } if (this.options.showLog) { - log('load ' + JSON.stringify(skus)); + log('load ' + JSON.stringify(skus)); } - return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "getProductDetails", [skus]); + cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "getProductDetails", [skus]); } }; @@ -140,4 +142,4 @@ window.inappbilling = new InAppBilling(); try { store.android = window.inappbilling; } catch (e) {} -}).call(this); +})(); diff --git a/src/js/platforms/ios-adapter.js b/src/js/platforms/ios-adapter.js index cae75ea6..630782d3 100644 --- a/src/js/platforms/ios-adapter.js +++ b/src/js/platforms/ios-adapter.js @@ -1,3 +1,4 @@ +/*global storekit */ (function() { "use strict"; @@ -119,7 +120,7 @@ store.when("expired", function(product) { //! var initialized = false; var initializing = false; -var storekitInit = function () { +function storekitInit() { if (initialized || initializing) return; initializing = true; store.log.debug("ios -> initializing storekit"); @@ -133,7 +134,7 @@ var storekitInit = function () { restoreCompleted: storekitRestoreCompleted, restoreFailed: storekitRestoreFailed }, storekitReady, storekitInitFailed); -}; +} //! //! ## *storekit* events handlers @@ -145,22 +146,22 @@ var storekitInit = function () { //! //! Loads all registered products, triggers `storekitLoaded()` when done. //! -var storekitReady = function () { +function storekitReady() { store.log.info("ios -> storekit ready"); initializing = false; initialized = true; storekitLoad(); -}; +} -var storekitInitFailed = function() { +function storekitInitFailed() { store.log.warn("ios -> storekit init failed"); initializing = false; retry(storekitInit); -}; +} var loaded = false; var loading = false; -var storekitLoad = function() { +function storekitLoad() { if (!initialized) return; if (loaded || loading) return; loading = true; @@ -169,7 +170,7 @@ var storekitLoad = function() { products.push(store.products[i].id); store.log.debug("ios -> loading products"); storekit.load(products, storekitLoaded, storekitLoadFailed); -}; +} //! ### *storekitLoaded()* //! @@ -180,7 +181,7 @@ var storekitLoad = function() { //! 3. Set the products state to `OWNED` (if it is so) //! 4. Set the store status to "ready". //! -var storekitLoaded = function (validProducts, invalidProductIds) { +function storekitLoaded(validProducts, invalidProductIds) { store.log.debug("ios -> products loaded"); var p; for (var i = 0; i < validProducts.length; ++i) { @@ -215,13 +216,13 @@ var storekitLoaded = function (validProducts, invalidProductIds) { storekit.loaded = true; store.ready(true); }, 1); -}; +} -var storekitLoadFailed = function() { +function storekitLoadFailed() { store.log.warn("ios -> loading products failed"); loading = false; retry(storekitLoad); -}; +} //! ### *storekitPurchasing()* //! @@ -229,7 +230,7 @@ var storekitLoadFailed = function() { //! //! It will set the product state to `INITIATED`. //! -var storekitPurchasing = function (productId) { +function storekitPurchasing(productId) { store.log.debug("ios -> is purchasing " + productId); store.ready(function() { var product = store.get(productId); @@ -240,7 +241,7 @@ var storekitPurchasing = function (productId) { if (product.state !== store.INITIATED) product.set("state", store.INITIATED); }); -}; +} //! ### *storekitPurchased()* //! @@ -249,7 +250,7 @@ var storekitPurchasing = function (productId) { //! It will set the product state to `APPROVED` and associates the product //! with the order's transaction identifier. //! -var storekitPurchased = function (transactionId, productId) { +function storekitPurchased(transactionId, productId) { store.ready(function() { var product = store.get(productId); if (!product) { @@ -279,7 +280,7 @@ var storekitPurchased = function (transactionId, productId) { store.log.info("ios -> transaction " + transactionId + " purchased (" + product.transactions.length + " in the queue for " + productId + ")"); product.set("state", store.APPROVED); }); -}; +} //! ### *storekitError()* //! @@ -287,9 +288,9 @@ var storekitPurchased = function (transactionId, productId) { //! //! Will convert storekit errors to a [`store.Error`](api.md/#errors). //! -var storekitError = function(errorCode, errorText, options) { +function storekitError(errorCode, errorText, options) { - var i,p; + var i, p; if (!options) options = {}; @@ -328,7 +329,7 @@ var storekitError = function(errorCode, errorText, options) { code: errorCode, message: errorText }); -}; +} // Restore purchases. // store.restore = function() { @@ -346,7 +347,7 @@ function storekitRestoreCompleted() { store.log.info("ios -> restore completed"); } -function storekitRestoreFailed(errorCode) { +function storekitRestoreFailed(/*errorCode*/) { store.log.warn("ios -> restore failed"); store.error({ code: store.ERR_REFRESH, @@ -369,7 +370,7 @@ store._prepareForValidation = function(product, callback) { }); }; -//! +//! //! ## Persistance of the *OWNED* status //! @@ -423,4 +424,4 @@ document.addEventListener("online", function() { } }, false); -}).call(this); +})(); diff --git a/src/js/platforms/ios-bridge.js b/src/js/platforms/ios-bridge.js index 4bf0ecf6..ea286dc6 100644 --- a/src/js/platforms/ios-bridge.js +++ b/src/js/platforms/ios-bridge.js @@ -1,4 +1,4 @@ -/** +/** * A plugin to enable iOS In-App Purchases. * * Copyright (c) Matt Kane 2011 @@ -6,14 +6,21 @@ * Copyright (c) Jean-Christophe Hoelt 2013 */ +/*eslint camelcase:0 */ +/*global cordova, window */ +(function(){ +"use strict"; + var exec = function (methodName, options, success, error) { cordova.exec(success, error, "InAppPurchase", methodName, options); }; var protectCall = function (callback, context) { - if (!callback) return; + if (!callback) { + return; + } try { - var args = Array.prototype.slice.call(arguments, 2); + var args = Array.prototype.slice.call(arguments, 2); callback.apply(this, args); } catch (err) { @@ -99,13 +106,13 @@ InAppPurchase.prototype.init = function (options, success, error) { }; /** - * Makes an in-app purchase. - * + * Makes an in-app purchase. + * * @param {String} productId The product identifier. e.g. "com.example.MyApp.myproduct" - * @param {int} quantity + * @param {int} quantity */ InAppPurchase.prototype.purchase = function (productId, quantity) { - quantity = (quantity|0) || 1; + quantity = (quantity | 0) || 1; var options = this.options; // Many people forget to load information about their products from apple's servers before allowing @@ -127,23 +134,23 @@ InAppPurchase.prototype.purchase = function (productId, quantity) { } }; var purchaseFailed = function () { - var msg = 'Purchasing ' + productId + ' failed'; - log(msg); + var errmsg = 'Purchasing ' + productId + ' failed'; + log(errmsg); if (typeof options.error === 'function') { - protectCall(options.error, 'options.error', InAppPurchase.prototype.ERR_PURCHASE, msg, productId, quantity); + protectCall(options.error, 'options.error', InAppPurchase.prototype.ERR_PURCHASE, errmsg, productId, quantity); } }; - return exec('purchase', [productId, quantity], purchaseOk, purchaseFailed); + exec('purchase', [productId, quantity], purchaseOk, purchaseFailed); }; /** * 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; - return exec('restoreCompletedTransactions', []); + exec('restoreCompletedTransactions', []); }; /** @@ -233,13 +240,13 @@ InAppPurchase.prototype.processPendingUpdates = function() { }; // 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) { if (!initialized) { - var args = Array.prototype.slice.call(arguments); + var args = Array.prototype.slice.call(arguments); pendingUpdates.push(args); return; } @@ -258,7 +265,7 @@ InAppPurchase.prototype.updatedTransactionCallback = function (state, errorCode, return; case "PaymentTransactionStatePurchased": protectCall(this.options.purchase, 'options.purchase', transactionIdentifier, productId); - return; + return; case "PaymentTransactionStateFailed": protectCall(this.options.error, 'options.error', errorCode, errorText, { productId: productId @@ -300,7 +307,7 @@ InAppPurchase.prototype.refreshReceipts = function() { var error = function(errMessage) { log('refresh receipt failed: ' + errMessage); - protectcall(options.error, 'options.error', InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); + protectCall(that.options.error, 'options.error', InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); }; exec('appStoreRefreshReceipt', [], loaded, error); @@ -318,10 +325,10 @@ InAppPurchase.prototype.loadReceipts = function (callback) { var error = function (errMessage) { log('load failed: ' + errMessage); - protectCall(options.error, 'options.error', InAppPurchase.prototype.ERR_LOAD_RECEIPTS, 'Failed to load receipt: ' + errMessage); + protectCall(that.options.error, 'options.error', InAppPurchase.prototype.ERR_LOAD_RECEIPTS, 'Failed to load receipt: ' + errMessage); }; - var callCallback = function () { + function callCallback() { if (callback) { protectCall(callback, 'loadReceipts.callback', { appStoreReceipt: that.appStoreReceipt, @@ -333,13 +340,13 @@ InAppPurchase.prototype.loadReceipts = function (callback) { } }); } - }; + } 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 + * 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. @@ -357,7 +364,7 @@ InAppPurchase.prototype.runQueue = function () { this.updatedTransactionCallback.apply(this, args); args = queue.shift(); } - if (!this.eventQueue.length) { + if (!this.eventQueue.length) { this.unWatchQueue(); } }; @@ -382,3 +389,4 @@ InAppPurchase.prototype.eventQueue = []; InAppPurchase.prototype.timer = null; window.storekit = new InAppPurchase(); +})(); diff --git a/src/js/product-internal.js b/src/js/product-internal.js index e0c91f7a..4b247b66 100644 --- a/src/js/product-internal.js +++ b/src/js/product-internal.js @@ -49,4 +49,4 @@ store.Product.prototype.trigger = function(action, args) { store.trigger(this, action, args); }; -}).call(this); +})(); diff --git a/src/js/product.js b/src/js/product.js index 9b4900e8..556f42de 100644 --- a/src/js/product.js +++ b/src/js/product.js @@ -1,8 +1,15 @@ (function() { 'use strict'; +function defer(thisArg, cb, delay) { + setTimeout(function() { + cb.call(thisArg); + }, delay || 1); +} +var delay = defer; + /// ## *store.Product* object ## -/// +/// /// Most events methods give you access to a `product` object. store.Product = function(options) { @@ -132,7 +139,7 @@ store.Product.prototype.verify = function() { var tryValidation = function() { // No need to verifiy a which status isn't approved - // It means it already has been + // It means it already has been if (that.state !== store.APPROVED) return; @@ -215,7 +222,7 @@ store.Product.prototype.verify = function() { /// - `error(function(err){})` /// - validation failed, either because of expiry or communication /// failure. - /// - `err` is a [store.Error object](#errors), with a code expected to be + /// - `err` is a [store.Error object](#errors), with a code expected to be /// `store.ERR_PAYMENT_EXPIRED` or `store.ERR_VERIFICATION_FAILED`. error: function(cb) { errorCb = cb; return this; } }; @@ -224,14 +231,7 @@ store.Product.prototype.verify = function() { return ret; }; -var defer = function(thisArg, cb, delay) { - setTimeout(function() { - cb.call(thisArg); - }, delay || 1); -}; -var delay = defer; - -/// +/// /// ### life-cycle /// /// A product will change state during the application execution. @@ -268,8 +268,8 @@ var delay = defer; /// #### state changes /// /// Each time the product changes state, appropriate events is triggered. -/// +/// /// Learn more about events [here](#events) and about listening to events [here](#when). /// -}).call(this); +})(); diff --git a/src/js/products.js b/src/js/products.js index a997a316..063c2e46 100644 --- a/src/js/products.js +++ b/src/js/products.js @@ -45,4 +45,4 @@ store.products.reset = function() { this.byId = {}; }; -}).call(this); +})(); diff --git a/src/js/queries.js b/src/js/queries.js index 62e55d02..74713230 100644 --- a/src/js/queries.js +++ b/src/js/queries.js @@ -42,7 +42,7 @@ store._queries = { callbacks: { /// #### *store._queries.callbacks.byQuery* dictionary /// Dictionary of: - /// + /// /// - *key*: a string equals to `query + " " + action` /// - *value*: array of callbacks /// @@ -55,7 +55,7 @@ store._queries = { /// #### *store._queries.callbacks.add(query, action, callback, once)* /// Simplify the query with `uniqueQuery()`, then add it to the dictionary. - /// + /// /// `action` is concatenated to the `query` string to create the key. add: function(query, action, cb, once) { var fullQuery = store._queries.uniqueQuery(query ? query + " " + action : action); @@ -112,12 +112,12 @@ store._queries = { /// The method generates all possible queries for the given `product` and `action`. var queries = []; - /// + /// /// - product.id + " " + action if (product && product.id) queries.push(product.id + " " + action); /// - product.alias + " " + action - if (product && product.alias && product.alias != product.id) + if (product && product.alias && product.alias !== product.id) queries.push(product.alias + " " + action); /// - product.type + " " + action if (product && product.type) @@ -133,7 +133,7 @@ store._queries = { queries.push("invalid " + action); /// - action queries.push(action); - + /// /// Then, for each query: /// @@ -162,14 +162,14 @@ store._queries = { store._queries.callbacks.byQuery[q] = cbs.filter(isNotOnce); } } - + /// /// **Note**: All events also trigger the `updated` event if (action !== "updated" && action !== 'error') this.triggerWhenProduct(product, "updated", [ product ]); } /// - + }; // isNotOnce return true iff a callback should be called more than once. @@ -181,4 +181,4 @@ function deferThrow(err) { setTimeout(function() { throw err; }, 1); } -}).call(this); +})(); diff --git a/src/js/ready.js b/src/js/ready.js index 43b50143..bdab3b2e 100644 --- a/src/js/ready.js +++ b/src/js/ready.js @@ -56,4 +56,4 @@ store.ready.reset = function() { callbacks = []; }; -}).call(this); +})(); diff --git a/src/js/refresh.js b/src/js/refresh.js index fe2851ab..539c4bce 100644 --- a/src/js/refresh.js +++ b/src/js/refresh.js @@ -48,7 +48,7 @@ store.refresh = function() { store.log.debug(" in state '" + p.state + "'"); // resend the "approved" event to all approved purchases. - // give user a chance to try delivering the content again and + // give user a chance to try delivering the content again and // finish the purchase. if (p.state === store.APPROVED) p.trigger(store.APPROVED); @@ -59,9 +59,9 @@ store.refresh = function() { else if (p.state === store.OWNED && (p.type === store.FREE_SUBSCRIPTION || p.type === store.PAID_SUBSCRIPTION)) p.set("state", store.APPROVED); } - + store.trigger("re-refreshed"); }; -}).call(this); +})(); diff --git a/src/js/register.js b/src/js/register.js index 35b1cb6e..973d32f7 100644 --- a/src/js/register.js +++ b/src/js/register.js @@ -18,8 +18,9 @@ store.register = function(product) { if (!product) return; if (!product.length) - return store.register([product]); - registerProducts(product); + store.register([product]); + else + registerProducts(product); }; /// ##### example usage @@ -95,4 +96,4 @@ function hasKeyword(string) { return false; } -}).call(this); +})(); diff --git a/src/js/store-ios.js b/src/js/store-ios.js index a8942c76..284efa12 100644 --- a/src/js/store-ios.js +++ b/src/js/store-ios.js @@ -1,5 +1,5 @@ //! # iOS Implementation -//! +//! //! The implementation of the unified API is a small layer //! built on top of the legacy "PhoneGap-InAppPurchase-iOS" plugin. //! diff --git a/src/js/store.js b/src/js/store.js index 57b3436f..a6dd4fbb 100644 --- a/src/js/store.js +++ b/src/js/store.js @@ -18,12 +18,12 @@ /// /// This event provides a generic way to track the statuses of your purchases, /// to unlock features when needed and to refresh your views accordingly. -/// +/// /// ### Registering products /// /// The store needs to know the type and identifiers of your products before you /// can use them in your code. -/// +/// /// Use [`store.register()`](#register) before your first call to /// [`store.refresh()`](#refresh). /// @@ -57,7 +57,7 @@ /// readable `title` and `description`, `price`, etc. /// /// This isn't an optional step as some despotic store owners (like Apple) require you -/// to display information about a product as retrieved from their server: no +/// to display information about a product as retrieved from their server: no /// hard-coding of price and title allowed! This is also convenient for you /// as you can change the price of your items knowing that it'll be reflected instantly /// on your clients' devices. @@ -76,9 +76,9 @@ /// render(); /// store.when("cc.fovea.test1").updated(render); /// } -/// +/// /// function render() { -/// +/// /// // Get the product from the pool. /// var product = store.get("cc.fovea.test1"); /// @@ -98,13 +98,13 @@ /// + "
" + product.description + "
" /// + "
" + product.price + "
" /// ); -/// +/// /// // Is this product owned? Give him a special class. /// if (product.owned) /// $el.addClass("owned"); /// else /// $el.removeClass("owned"); -/// +/// /// // Is an order for this product in progress? Can't be ordered right now? /// if (product.canPurchase) /// $el.addClass("can-purchase"); @@ -112,7 +112,7 @@ /// $el.removeClass("can-purchase"); /// } /// } -/// +/// /// // method called when the view is hidden /// function hide() { /// // stop monitoring the product @@ -174,7 +174,7 @@ /// #### simple case /// /// In the most simple case, where: -/// +/// /// - delivery of purchases is only local ; /// - you don't want to implement receipt validation ; /// @@ -225,7 +225,7 @@ /// /// If the validator returns a `store.PURCHASE_EXPIRED` error code, the subscription will /// automatically loose its `owned` status. -/// +/// /// Typically, you'll enable and disable access to your content this way. /// ```js /// store.when("cc.fovea.subcription").updated(function(product) { @@ -237,26 +237,26 @@ /// ``` // ### Security -// +// // You will initiate a purchase with `store.order("product.id")`. -// +// // 99% of the times, the purchase will be approved immediately by billing system. // // However, connection can be lost between you sending a purchase request // and the server answering to you. In that case, the purchase shouldn't // be lost (because the user paid for it), that's why the store will notify // you of an approved purchase during the next application startup. -// +// // The same can also happen if the user bought a product from another device, using his // same account. -// -// For that reason, you should register all your features-unlocking listeners at +// +// For that reason, you should register all your features-unlocking listeners at // startup, before the first call to `store.refresh()` // -/// +/// /// # *store* object ## -/// +/// /// `store` is the global object exported by the purchase plugin. /// /// As with any other plugin, this object shouldn't be used before @@ -270,7 +270,7 @@ var store = {}; /// ## *store.verbosity* /// /// The `verbosity` property defines how much you want `store.js` to write on the console. Set to: -/// +/// /// - `store.QUIET` or `0` to disable all logging (default) /// - `store.ERROR` or `1` to show only error messages /// - `store.WARNING` or `2` to show warnings and errors diff --git a/src/js/trigger.js b/src/js/trigger.js index b6fac7dc..5719914c 100644 --- a/src/js/trigger.js +++ b/src/js/trigger.js @@ -41,4 +41,4 @@ store.trigger = function(product, action, args) { store._queries.triggerWhenProduct(product, action, args); }; -}).call(this); +})(); diff --git a/src/js/utils.js b/src/js/utils.js index c8d04830..6e6fa00c 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -56,17 +56,17 @@ store.utils = { /// /// Options: /// - /// * `url`: + /// * `url`: /// * `method`: HTTP method to use (GET, POST, ...) /// * `success`: callback(data) /// * `error`: callback(statusCode, statusText) /// * `data`: body of your request - /// + /// ajax: function(options) { var doneCb = function(){}; var xhr = new XMLHttpRequest(); xhr.open(options.method || 'POST', options.url, true); - xhr.onreadystatechange = function(event) { + xhr.onreadystatechange = function(/*event*/) { try { if (xhr.readyState === 4) { if (xhr.status === 200) { @@ -99,4 +99,4 @@ store.utils = { } }; -}).call(this); +})(); diff --git a/src/js/validator.js b/src/js/validator.js index 6923093b..cfe045be 100644 --- a/src/js/validator.js +++ b/src/js/validator.js @@ -13,7 +13,7 @@ /// ```js /// store.validator = "http://store.fovea.cc:1980/check-purchase"; /// ``` -/// +/// /// ```js /// store.validator = function(product, callback) { /// @@ -38,9 +38,9 @@ /// Validation error codes are [documented here](#validation-error-codes). store.validator = null; -// +// // ## store._validator -// +// // Execute the internal validation call, either to a webservice // or to the provided callback. // @@ -86,10 +86,10 @@ store._validator = function(product, callback, isPrepared) { /// - store specific data /// /// Refer to [this documentation for iOS](https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1). -/// +/// /// Start [here for Android](https://developer.android.com/google/play/billing/billing_integrate.html#billing-security). /// /// Another option is to use [Fovea's reeceipt validation service](http://reeceipt.fovea.cc/) that implements all the best practices to secure your transactions. /// -}).call(this); +})(); diff --git a/src/js/when.js b/src/js/when.js index abaedf78..4fcb111a 100644 --- a/src/js/when.js +++ b/src/js/when.js @@ -2,11 +2,11 @@ 'use strict'; /// ## *store.when(query)* -/// +/// /// Register a callback for a product-related event. /// store.when = function(query, once, callback) { - + // No arguments, will match all products. if (typeof query === 'undefined') query = ''; @@ -28,7 +28,7 @@ store.when = function(query, once, callback) { }; }; - /// + /// /// ### return value /// /// Return a Promise with methods to register callbacks for @@ -61,7 +61,7 @@ store.when = function(query, once, callback) { /// - `cancelled(product)` /// - Called when a product [order](#order) is cancelled by the user. addPromise('cancelled'); - + /// - `refunded(product)` /// - Called when an order is refunded by the user. addPromise('refunded'); @@ -131,7 +131,7 @@ store.when.unregister = function(cb) { /// - `"subscription"` - all subscriptions. /// - `"free subscription"` - all free subscriptions. /// - `"paid subscription"` - all paid subscriptions. -/// +/// /// Filter by product state: /// /// - `"valid"` - all products in the VALID state. @@ -155,6 +155,6 @@ store.when.unregister = function(cb) { /// - equivalent to just `"cc.fovea.inapp1"` /// - `"invalid product"` - an invalid product /// - equivalent to just `"invalid"` -/// +/// -}).call(this); +})(); diff --git a/test/js/helper.js b/test/js/helper.js index 9d8729e1..2b9bf73a 100644 --- a/test/js/helper.js +++ b/test/js/helper.js @@ -1,18 +1,22 @@ -var timeFactor = 1; -var pSetTimeout = global.setTimeout; -global.setTimeout = function(fn, delay) { - pSetTimeout(fn, delay / timeFactor); -}; +(function(){ + "use strict"; -exports.resetTest = function() { - var store = require("../tmp/store-test"); - store._queries.callbacks.byQuery = {}; - store.ready.reset(); - store.products.reset(); - store.error.callbacks.reset(); - timeFactor = 1; -}; + var timeFactor = 1; + var pSetTimeout = global.setTimeout; + global.setTimeout = function(fn, delay) { + pSetTimeout(fn, delay / timeFactor); + }; -exports.setTimeoutFactor = function(value) { - timeFactor = value; -}; + exports.resetTest = function() { + var store = require("../tmp/store-test"); + store._queries.callbacks.byQuery = {}; + store.ready.reset(); + store.products.reset(); + store.error.callbacks.reset(); + timeFactor = 1; + }; + + exports.setTimeoutFactor = function(value) { + timeFactor = value; + }; +})(); diff --git a/test/js/run.js b/test/js/run.js index 1960ee49..d39dc9b8 100644 --- a/test/js/run.js +++ b/test/js/run.js @@ -1,26 +1,29 @@ -var Mocha = require('mocha'); +(function() { + "use strict"; + var Mocha = require('mocha'); -var mocha = new Mocha({ - ui: 'bdd' -}); + var mocha = new Mocha({ + ui: 'bdd' + }); -mocha.reporter('list'); + mocha.reporter('list'); -mocha.addFile('test/js/test-ready.js'); -mocha.addFile('test/js/test-error.js'); -mocha.addFile('test/js/test-off.js'); -mocha.addFile('test/js/test-order.js'); -mocha.addFile('test/js/test-queries.js'); -mocha.addFile('test/js/test-register.js'); -mocha.addFile('test/js/test-when.js'); -mocha.addFile('test/js/test-finish.js'); -mocha.addFile('test/js/test-verify.js'); -mocha.addFile('test/js/test-utils.js'); -mocha.addFile('test/js/test-ios.js'); -mocha.addFile('test/js/test-android.js'); + mocha.addFile('test/js/test-ready.js'); + mocha.addFile('test/js/test-error.js'); + mocha.addFile('test/js/test-off.js'); + mocha.addFile('test/js/test-order.js'); + mocha.addFile('test/js/test-queries.js'); + mocha.addFile('test/js/test-register.js'); + mocha.addFile('test/js/test-when.js'); + mocha.addFile('test/js/test-finish.js'); + mocha.addFile('test/js/test-verify.js'); + mocha.addFile('test/js/test-utils.js'); + mocha.addFile('test/js/test-ios.js'); + mocha.addFile('test/js/test-android.js'); -mocha.run(function(){ - console.log('done'); -}).on('pass', function(test){ - // console.log('... %s', test.title); -}); + mocha.run(function(){ + console.log('done'); + }).on('pass', function(/*test*/){ + // console.log('... %s', test.title); + }); +})(); diff --git a/test/js/test-android.js b/test/js/test-android.js index d85d2f61..b9092d7f 100644 --- a/test/js/test-android.js +++ b/test/js/test-android.js @@ -1,19 +1,23 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); global.store = store; global.document = { - addEventListener: function(event, callback) {} + addEventListener: function(/*event, callback*/) { + "use strict"; + } }; global.localStorage = {}; require("../tmp/android-adapter"); describe('Android', function(){ + "use strict"; describe('#init', function(){ it('should', function() { assert.ok(true); }); }); }); - diff --git a/test/js/test-error.js b/test/js/test-error.js index 856cb9c5..eb56d573 100644 --- a/test/js/test-error.js +++ b/test/js/test-error.js @@ -1,7 +1,10 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Errors', function(){ + "use strict"; beforeEach(function() { store.error.callbacks.reset(); diff --git a/test/js/test-finish.js b/test/js/test-finish.js index 92358a71..2f16214e 100644 --- a/test/js/test-finish.js +++ b/test/js/test-finish.js @@ -1,7 +1,11 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Finish', function() { + "use strict"; + before(function() { require("./helper").resetTest(); store.register({ @@ -19,6 +23,7 @@ describe('Finish', function() { p.set('state', store.VALID); }); store.once(p).valid(function(p) { + assert.equal("pf-consumable", p.id); done(); }); p.finish(); diff --git a/test/js/test-ios.js b/test/js/test-ios.js index 46cc8f34..11291fbe 100644 --- a/test/js/test-ios.js +++ b/test/js/test-ios.js @@ -1,10 +1,12 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach, storekit */ var assert = require("assert"); var store = require("../tmp/store-test"); var helper = require("./helper"); global.store = store; global.document = { - addEventListener: function(event, callback) {} + addEventListener: function(/*event, callback*/) { "use strict"; } }; global.localStorage = {}; @@ -12,6 +14,7 @@ global.localStorage = {}; global.storekit = { initShouldFail: false, init: function(options, success, error) { + "use strict"; this.options = options; this.initCalled = (this.initCalled || 0) + 1; if (this.initShouldFail) { @@ -25,6 +28,7 @@ global.storekit = { }, loadShouldFail: false, load: function(products, success, error) { + "use strict"; this.products = products; this.loadCalled = (this.loadCalled || 0) + 1; if (this.loadShouldFail) { @@ -43,6 +47,7 @@ global.storekit = { }; describe('iOS', function(){ + "use strict"; before(helper.resetTest); after(helper.resetTest); diff --git a/test/js/test-off.js b/test/js/test-off.js index e2500082..5fd03169 100644 --- a/test/js/test-off.js +++ b/test/js/test-off.js @@ -1,7 +1,10 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Off', function(){ + "use strict"; var product = { id: "p1", @@ -92,4 +95,3 @@ describe('Off', function(){ }); }); - diff --git a/test/js/test-order.js b/test/js/test-order.js index 22f10f28..efeb7663 100644 --- a/test/js/test-order.js +++ b/test/js/test-order.js @@ -1,7 +1,10 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Order', function(){ + "use strict"; describe('#order()', function(){ diff --git a/test/js/test-queries.js b/test/js/test-queries.js index 9198d187..1d4ca2ed 100644 --- a/test/js/test-queries.js +++ b/test/js/test-queries.js @@ -1,7 +1,10 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Queries', function(){ + "use strict"; describe('#uniqueQuery()', function(){ @@ -41,12 +44,12 @@ describe('Queries', function(){ approved = true; }); assert.equal(false, approved); - store._queries.triggerWhenProduct({id:"full version"}, "approved"); + store._queries.triggerWhenProduct({id: "full version"}, "approved"); assert.equal(true, approved); approved = false; assert.equal(false, approved); - store._queries.triggerWhenProduct({id:"full version"}, "approved"); + store._queries.triggerWhenProduct({id: "full version"}, "approved"); assert.equal(true, approved); }); @@ -58,12 +61,12 @@ describe('Queries', function(){ approved = true; }, true); assert.equal(false, approved); - store._queries.triggerWhenProduct({id:"lite version"}, "approved"); + store._queries.triggerWhenProduct({id: "lite version"}, "approved"); assert.equal(true, approved); approved = false; assert.equal(false, approved); - store._queries.triggerWhenProduct({id:"lite version"}, "approved"); + store._queries.triggerWhenProduct({id: "lite version"}, "approved"); assert.equal(false, approved); }); @@ -75,30 +78,34 @@ describe('Queries', function(){ approved = true; }); assert.equal(false, approved); - store._queries.triggerWhenProduct({id:"anything", type:store.FREE_SUBSCRIPTION}, "approved"); + store._queries.triggerWhenProduct({id: "anything", type: store.FREE_SUBSCRIPTION}, "approved"); assert.equal(true, approved); }); it('should execute all callbacks even if one fails with an exception', function() { - var f1_called = false; - var f1_returned = false; - var f2_called = false; - var f2_returned = false; + var f1Called = false; + var f1Returned = false; + var f2Called = false; + var f2Returned = false; store._queries.callbacks.add("", "yyy", function() { - f1_called = true; - if (f1_called) throw "ERROR"; - f1_returned = true; + f1Called = true; + if (f1Called) { + throw "ERROR"; + } + f1Returned = true; }, true); store._queries.callbacks.add("", "yyy", function() { - f2_called = true; - if (f2_called) throw "ERROR"; - f2_returned = true; + f2Called = true; + if (f2Called) { + throw "ERROR"; + } + f2Returned = true; }, true); store._queries.triggerAction("yyy"); - assert.equal(true, f1_called); - assert.equal(false, f1_returned); - assert.equal(true, f2_called); - assert.equal(false, f2_returned); + assert.equal(true, f1Called); + assert.equal(false, f1Returned); + assert.equal(true, f2Called); + assert.equal(false, f2Returned); }); it('should trigger an update for any event', function(done) { @@ -106,24 +113,24 @@ describe('Queries', function(){ assert.equal("test.updated1", product.id); done(); }); - store._queries.triggerWhenProduct({id:"test.updated1"}, "xyz"); + store._queries.triggerWhenProduct({id: "test.updated1"}, "xyz"); }); it('should not trigger an update for error events', function(done) { - store._queries.callbacks.add("product", "updated", function(product) { - console.log("updated2"); + store._queries.callbacks.add("product", "updated", function(/*product*/) { assert(false, "updated shouldn't be called"); }); - store._queries.triggerWhenProduct({id:"test.updated2"}, "error"); + store._queries.triggerWhenProduct({id: "test.updated2"}, "error"); setTimeout(done, 6); }); it('should not trigger "updated" events twice', function(done) { var nCalls = 0; store._queries.callbacks.add("product", "updated", function(product) { + assert.equal("test.updated2", product.id); nCalls++; }); - store._queries.triggerWhenProduct({id:"test.updated2"}, "udpated"); + store._queries.triggerWhenProduct({id: "test.updated2"}, "udpated"); setTimeout(function() { assert.equal(1, nCalls); done(); diff --git a/test/js/test-ready.js b/test/js/test-ready.js index b960c199..64cc7da3 100644 --- a/test/js/test-ready.js +++ b/test/js/test-ready.js @@ -1,7 +1,10 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Ready', function(){ + "use strict"; describe('#ready()', function(){ diff --git a/test/js/test-register.js b/test/js/test-register.js index d29a0add..f289c7db 100644 --- a/test/js/test-register.js +++ b/test/js/test-register.js @@ -1,7 +1,14 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Register', function(){ + "use strict"; + + var helper = require("./helper"); + before(helper.resetTest); + after(helper.resetTest); describe('#register()', function(){ @@ -43,4 +50,3 @@ describe('Register', function(){ }); }); }); - diff --git a/test/js/test-utils.js b/test/js/test-utils.js index 477cef02..592446d9 100644 --- a/test/js/test-utils.js +++ b/test/js/test-utils.js @@ -1,18 +1,21 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); describe('Utils', function(){ + "use strict"; describe('#ajax', function(){ - XMLHttpRequest = function() { + global.XMLHttpRequest = function() { this.open = function(method, url) { this.method = method; this.url = url; }; - this.setRequestHeader = function(key,value) {}; - this.onreadystatechange = function(event) {}; - this.send = function(data) { + this.setRequestHeader = function(/*key,value*/) {}; + this.onreadystatechange = function(/*event*/) {}; + this.send = function(/*data*/) { this.readyState = 1; this.onreadystatechange({}); this.readyState = 2; @@ -43,7 +46,7 @@ describe('Utils', function(){ assert.ok(data.success, "request should send success status"); done(); }, - error: function(status, message) { + error: function(/*status, message*/) { assert.ok(false, "this request shouldn't fail"); } }); @@ -54,11 +57,12 @@ describe('Utils', function(){ url: 'error404', method: 'POST', data: { a: 1, b: 2 }, - success: function(data) { + success: function(/*data*/) { assert.ok(false, "request should not send success status"); }, error: function(status, message) { assert.equal(404, status, "this request should fail with status 404"); + assert.ok(message, "this request should send an error message"); done(); } }); @@ -69,10 +73,10 @@ describe('Utils', function(){ url: 'dummy', method: 'POST', data: { a: 1, b: 2 }, - success: function(data) { - a.b.c = 0; + success: function(/*data*/) { + throw new Error(); }, - error: function(status, message) { + error: function(/*status, message*/) { assert.ok(false, "Error callback shoudn't be called"); } }).done(function() { @@ -81,4 +85,3 @@ describe('Utils', function(){ }); }); }); - diff --git a/test/js/test-verify.js b/test/js/test-verify.js index 5dacb100..de0df255 100644 --- a/test/js/test-verify.js +++ b/test/js/test-verify.js @@ -1,8 +1,12 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); var helper = require("./helper"); describe('Verify', function() { + "use strict"; + before(function() { require("./helper").resetTest(); store.register({ @@ -34,8 +38,9 @@ describe('Verify', function() { callback(true, product); }; - step1(); - function step1() { + var step1, step2; + + step1 = function() { p.set('state', store.VALID); var doneCalled = 0; var errorCalled = 0; @@ -51,9 +56,9 @@ describe('Verify', function() { assert.equal(1, errorCalled, "error should be called"); step2(); }, 1500); - } + }; - function step2() { + step2 = function() { p.set('state', store.APPROVED); var successCalled = 0; var doneCalled = 0; @@ -68,7 +73,9 @@ describe('Verify', function() { assert.equal(1, doneCalled, "done should be called"); done(); }, 1500); - } + }; + + step1(); }); it('should retry 5 times when validator fails', function(done) { @@ -80,7 +87,6 @@ describe('Verify', function() { }; p.set('state', store.APPROVED); helper.setTimeoutFactor(4000); - step(); function step() { var successCalled = 0; @@ -105,7 +111,8 @@ describe('Verify', function() { done(); }, 200000); } + + step(); }); }); }); - diff --git a/test/js/test-when.js b/test/js/test-when.js index aa6bda34..6077b55b 100644 --- a/test/js/test-when.js +++ b/test/js/test-when.js @@ -1,8 +1,11 @@ +/*eslint-env mocha */ +/*global describe, it, before, beforeEach, after, afterEach */ var assert = require("assert"); var store = require("../tmp/store-test"); var helper = require("./helper"); describe('When', function(){ + "use strict"; var product = { id: "p1", @@ -24,7 +27,6 @@ describe('When', function(){ approved(nop). updated(nop). cancelled(nop); - }); it('should be called on id', function(){ @@ -33,15 +35,16 @@ describe('When', function(){ var loaded = false; store.when("p1").loaded(function(product) { loaded = true; + assert.equal(product.id, "p1"); }); - assert.equal(loaded, false); - store._queries.triggerWhenProduct(product, "loaded"); - assert.equal(loaded, true); + assert.equal(loaded, false, "loaded should not be called initially"); + store._queries.triggerWhenProduct(product, "loaded", [product]); + assert.equal(loaded, true, "loaded should be called after the 'loaded' event"); loaded = false; - store._queries.triggerWhenProduct(product, "loaded"); - assert.equal(loaded, true); + store._queries.triggerWhenProduct(product, "loaded", [product]); + assert.equal(loaded, true, "loaded should be called every 'loaded' event"); }); it('should be called on aliases', function(){ @@ -49,15 +52,16 @@ describe('When', function(){ var loaded = false; store.when("product").loaded(function(product) { + assert.equal(product.id, "p1"); loaded = true; }); assert.equal(loaded, false); - store._queries.triggerWhenProduct(product, "loaded"); + store._queries.triggerWhenProduct(product, "loaded", [product]); assert.equal(loaded, true); loaded = false; - store._queries.triggerWhenProduct(product, "loaded"); + store._queries.triggerWhenProduct(product, "loaded", [product]); assert.equal(loaded, true); }); @@ -89,15 +93,16 @@ describe('When', function(){ var loaded = false; store.once("p1").loaded(function(product) { + assert.equal(product.id, "p1"); loaded = true; }); assert.equal(loaded, false); - store._queries.triggerWhenProduct(product, "loaded"); + store._queries.triggerWhenProduct(product, "loaded", [product]); assert.equal(loaded, true); loaded = false; - store._queries.triggerWhenProduct(product, "loaded"); + store._queries.triggerWhenProduct(product, "loaded", [product]); assert.equal(loaded, false); }); @@ -106,15 +111,16 @@ describe('When', function(){ var loaded = false; store.once("product").loaded(function(product) { + assert.equal(product.id, "p1"); loaded = true; }); assert.equal(loaded, false); - store._queries.triggerWhenProduct(product, "loaded"); + store._queries.triggerWhenProduct(product, "loaded", [product]); assert.equal(loaded, true); loaded = false; - store._queries.triggerWhenProduct(product, "loaded"); + store._queries.triggerWhenProduct(product, "loaded", [product]); assert.equal(loaded, false); }); }); diff --git a/www/store-android.js b/www/store-android.js index f4fedccd..55924677 100644 --- a/www/store-android.js +++ b/www/store-android.js @@ -44,10 +44,16 @@ store.verbosity = 0; store.INVALID_PAYLOAD = 6778001; store.CONNECTION_FAILED = 6778002; store.PURCHASE_EXPIRED = 6778003; -}).call(this); +})(); (function() { "use strict"; + function defer(thisArg, cb, delay) { + setTimeout(function() { + cb.call(thisArg); + }, delay || 1); + } + var delay = defer; store.Product = function(options) { if (!options) options = {}; this.id = options.id || null; @@ -78,10 +84,11 @@ store.verbosity = 0; store.Product.prototype.verify = function() { var that = this; var nRetry = 0; - var doneCb = function() {}; - var successCb = function() {}; - var expiredCb = function() {}; - var errorCb = function() {}; + var noop = function() {}; + var doneCb = noop; + var successCb = noop; + var expiredCb = noop; + var errorCb = noop; var tryValidation = function() { if (that.state !== store.APPROVED) return; store._validator(that, function(success, data) { @@ -94,20 +101,20 @@ store.verbosity = 0; } else { store.log.debug("verify -> error: " + JSON.stringify(data)); var msg = data && data.error && data.error.message ? data.error.message : ""; - var err = new Error({ + var err = new store.Error({ code: store.ERR_VERIFICATION_FAILED, message: "Transaction verification failed: " + msg }); if (data.code === store.PURCHASE_EXPIRED) { - err = new Error({ + err = new store.Error({ code: store.ERR_PAYMENT_EXPIRED, message: "Transaction expired: " + msg }); } - store.error(err); - store.utils.callExternal("verify.error", errorCb, err); - store.utils.callExternal("verify.done", doneCb, that); if (data.code === store.PURCHASE_EXPIRED) { + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); that.trigger("expired"); that.set("state", store.VALID); store.utils.callExternal("verify.expired", expiredCb, that); @@ -115,11 +122,27 @@ store.verbosity = 0; nRetry += 1; delay(this, tryValidation, 1e3 * nRetry * nRetry); } else { + store.log.debug("validation failed 5 times, stop retrying, trigger an error"); + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); that.trigger("unverified"); } } }); }; + defer(this, function() { + if (that.state !== store.APPROVED) { + var err = new store.Error({ + code: store.ERR_VERIFICATION_FAILED, + message: "Product isn't in the APPROVED state" + }); + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); + return; + } + }); delay(this, tryValidation, 1e3); var ret = { done: function(cb) { @@ -141,13 +164,7 @@ store.verbosity = 0; }; return ret; }; - var defer = function(thisArg, cb, delay) { - window.setTimeout(function() { - cb.call(thisArg); - }, delay || 1); - }; - var delay = defer; -}).call(this); +})(); (function() { "use strict"; @@ -169,14 +186,13 @@ store.verbosity = 0; store.error.unregister = function(cb) { store.error.callbacks.unregister(cb); }; -}).call(this); +})(); (function() { "use strict"; store.register = function(product) { if (!product) return; - if (!product.length) return store.register([ product ]); - registerProducts(product); + if (!product.length) store.register([ product ]); else registerProducts(product); }; function registerProducts(products) { for (var i = 0; i < products.length; ++i) { @@ -201,7 +217,7 @@ store.verbosity = 0; } return false; } -}).call(this); +})(); (function() { "use strict"; @@ -209,7 +225,7 @@ store.verbosity = 0; var product = store.products.byId[id] || store.products.byAlias[id]; return product; }; -}).call(this); +})(); (function() { "use strict"; @@ -250,7 +266,7 @@ store.verbosity = 0; store.when.unregister = function(cb) { store._queries.callbacks.unregister(cb); }; -}).call(this); +})(); (function() { "use strict"; @@ -264,14 +280,13 @@ store.verbosity = 0; } }; store.once.unregister = store.when.unregister; -}).call(this); +})(); (function() { "use strict"; var callbacks = {}; var callbackId = 0; store.order = function(pid) { - var that = this; var p = pid; if (typeof pid === "string") { p = store.products.byId[pid] || store.products.byAlias[pid]; @@ -308,7 +323,7 @@ store.verbosity = 0; store.once(p.id, "error", function(err) { if (!localCallback.error) return; done(); - cb(p); + cb(err); }); return this; } @@ -320,7 +335,7 @@ store.verbosity = 0; if (callbacks[i].error === cb) delete callbacks[i].error; } }; -}).call(this); +})(); (function() { "use strict"; @@ -355,7 +370,7 @@ store.verbosity = 0; isReady = false; callbacks = []; }; -}).call(this); +})(); (function() { "use strict"; @@ -365,7 +380,7 @@ store.verbosity = 0; store.order.unregister(callback); store.error.unregister(callback); }; -}).call(this); +})(); (function() { "use strict"; @@ -394,7 +409,7 @@ store.verbosity = 0; store.validator(product, callback); } }; -}).call(this); +})(); (function() { "use strict"; @@ -414,7 +429,7 @@ store.verbosity = 0; } store.trigger("re-refreshed"); }; -}).call(this); +})(); (function() { "use strict"; @@ -443,7 +458,7 @@ store.verbosity = 0; log(store.DEBUG, o); } }; -}).call(this); +})(); (function() { "use strict"; @@ -460,7 +475,7 @@ store.verbosity = 0; this.byAlias = {}; this.byId = {}; }; -}).call(this); +})(); (function() { "use strict"; @@ -496,7 +511,7 @@ store.verbosity = 0; store.Product.prototype.trigger = function(action, args) { store.trigger(this, action, args); }; -}).call(this); +})(); (function() { "use strict"; @@ -551,7 +566,7 @@ store.verbosity = 0; triggerWhenProduct: function(product, action, args) { var queries = []; if (product && product.id) queries.push(product.id + " " + action); - if (product && product.alias && product.alias != product.id) queries.push(product.alias + " " + action); + if (product && product.alias && product.alias !== product.id) queries.push(product.alias + " " + action); if (product && product.type) queries.push(product.type + " " + action); if (product && product.type && (product.type === store.FREE_SUBSCRIPTION || product.type === store.PAID_SUBSCRIPTION)) queries.push("subscription " + action); if (product && product.valid === true) queries.push("valid " + action); @@ -585,7 +600,7 @@ store.verbosity = 0; throw err; }, 1); } -}).call(this); +})(); (function() { "use strict"; @@ -607,7 +622,7 @@ store.verbosity = 0; } store._queries.triggerWhenProduct(product, action, args); }; -}).call(this); +})(); (function() { "use strict"; @@ -639,7 +654,7 @@ store.verbosity = 0; throw err; }, 1); } -}).call(this); +})(); (function() { "use strict"; @@ -654,7 +669,6 @@ store.verbosity = 0; }, callExternal: function(name, callback) { try { - store.log.debug("calling " + name); var args = Array.prototype.slice.call(arguments, 2); if (callback) callback.apply(this, args); } catch (e) { @@ -665,7 +679,7 @@ store.verbosity = 0; var doneCb = function() {}; var xhr = new XMLHttpRequest(); xhr.open(options.method || "POST", options.url, true); - xhr.onreadystatechange = function(event) { + xhr.onreadystatechange = function() { try { if (xhr.readyState === 4) { if (xhr.status === 200) { @@ -696,7 +710,7 @@ store.verbosity = 0; }; } }; -}).call(this); +})(); (function() { "use strict"; @@ -735,9 +749,9 @@ store.verbosity = 0; } } if (hasSKUs) { - return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", [ skus ]); + cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", [ skus ]); } else { - return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", []); + cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "init", []); } }; InAppBilling.prototype.getPurchases = function(success, fail) { @@ -789,7 +803,7 @@ store.verbosity = 0; if (this.options.showLog) { log("load " + JSON.stringify(skus)); } - return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "getProductDetails", [ skus ]); + cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "getProductDetails", [ skus ]); } }; function errorCb(fail) { @@ -809,19 +823,19 @@ store.verbosity = 0; try { store.android = window.inappbilling; } catch (e) {} -}).call(this); +})(); (function() { "use strict"; + var initialized = false; + var skus = []; store.when("refreshed", function() { if (!initialized) init(); }); store.when("re-refreshed", function() { iabGetPurchases(); }); - var initialized = false; - var skus = []; - var init = function() { + function init() { if (initialized) return; initialized = true; for (var i = 0; i < store.products.length; ++i) skus.push(store.products[i].id); @@ -833,7 +847,7 @@ store.verbosity = 0; }, { showLog: store.verbosity >= store.DEBUG ? true : false }, skus); - }; + } function iabReady() { store.log.debug("android -> ready"); store.android.getAvailableProducts(iabLoaded, function(err) { @@ -955,7 +969,7 @@ store.verbosity = 0; product.set("state", store.VALID); }, function(err, code) { store.error({ - code: code || ERR_UNKNOWN, + code: code || store.ERR_UNKNOWN, message: err }); }, product.id); @@ -963,7 +977,7 @@ store.verbosity = 0; product.set("state", store.OWNED); } }); -}).call(this); +})(); if (window) { window.store = store; diff --git a/www/store-ios.js b/www/store-ios.js index 7eae3bcd..8e084ed6 100644 --- a/www/store-ios.js +++ b/www/store-ios.js @@ -44,10 +44,16 @@ store.verbosity = 0; store.INVALID_PAYLOAD = 6778001; store.CONNECTION_FAILED = 6778002; store.PURCHASE_EXPIRED = 6778003; -}).call(this); +})(); (function() { "use strict"; + function defer(thisArg, cb, delay) { + setTimeout(function() { + cb.call(thisArg); + }, delay || 1); + } + var delay = defer; store.Product = function(options) { if (!options) options = {}; this.id = options.id || null; @@ -78,10 +84,11 @@ store.verbosity = 0; store.Product.prototype.verify = function() { var that = this; var nRetry = 0; - var doneCb = function() {}; - var successCb = function() {}; - var expiredCb = function() {}; - var errorCb = function() {}; + var noop = function() {}; + var doneCb = noop; + var successCb = noop; + var expiredCb = noop; + var errorCb = noop; var tryValidation = function() { if (that.state !== store.APPROVED) return; store._validator(that, function(success, data) { @@ -94,20 +101,20 @@ store.verbosity = 0; } else { store.log.debug("verify -> error: " + JSON.stringify(data)); var msg = data && data.error && data.error.message ? data.error.message : ""; - var err = new Error({ + var err = new store.Error({ code: store.ERR_VERIFICATION_FAILED, message: "Transaction verification failed: " + msg }); if (data.code === store.PURCHASE_EXPIRED) { - err = new Error({ + err = new store.Error({ code: store.ERR_PAYMENT_EXPIRED, message: "Transaction expired: " + msg }); } - store.error(err); - store.utils.callExternal("verify.error", errorCb, err); - store.utils.callExternal("verify.done", doneCb, that); if (data.code === store.PURCHASE_EXPIRED) { + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); that.trigger("expired"); that.set("state", store.VALID); store.utils.callExternal("verify.expired", expiredCb, that); @@ -115,11 +122,27 @@ store.verbosity = 0; nRetry += 1; delay(this, tryValidation, 1e3 * nRetry * nRetry); } else { + store.log.debug("validation failed 5 times, stop retrying, trigger an error"); + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); that.trigger("unverified"); } } }); }; + defer(this, function() { + if (that.state !== store.APPROVED) { + var err = new store.Error({ + code: store.ERR_VERIFICATION_FAILED, + message: "Product isn't in the APPROVED state" + }); + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); + return; + } + }); delay(this, tryValidation, 1e3); var ret = { done: function(cb) { @@ -141,13 +164,7 @@ store.verbosity = 0; }; return ret; }; - var defer = function(thisArg, cb, delay) { - window.setTimeout(function() { - cb.call(thisArg); - }, delay || 1); - }; - var delay = defer; -}).call(this); +})(); (function() { "use strict"; @@ -169,14 +186,13 @@ store.verbosity = 0; store.error.unregister = function(cb) { store.error.callbacks.unregister(cb); }; -}).call(this); +})(); (function() { "use strict"; store.register = function(product) { if (!product) return; - if (!product.length) return store.register([ product ]); - registerProducts(product); + if (!product.length) store.register([ product ]); else registerProducts(product); }; function registerProducts(products) { for (var i = 0; i < products.length; ++i) { @@ -201,7 +217,7 @@ store.verbosity = 0; } return false; } -}).call(this); +})(); (function() { "use strict"; @@ -209,7 +225,7 @@ store.verbosity = 0; var product = store.products.byId[id] || store.products.byAlias[id]; return product; }; -}).call(this); +})(); (function() { "use strict"; @@ -250,7 +266,7 @@ store.verbosity = 0; store.when.unregister = function(cb) { store._queries.callbacks.unregister(cb); }; -}).call(this); +})(); (function() { "use strict"; @@ -264,14 +280,13 @@ store.verbosity = 0; } }; store.once.unregister = store.when.unregister; -}).call(this); +})(); (function() { "use strict"; var callbacks = {}; var callbackId = 0; store.order = function(pid) { - var that = this; var p = pid; if (typeof pid === "string") { p = store.products.byId[pid] || store.products.byAlias[pid]; @@ -308,7 +323,7 @@ store.verbosity = 0; store.once(p.id, "error", function(err) { if (!localCallback.error) return; done(); - cb(p); + cb(err); }); return this; } @@ -320,7 +335,7 @@ store.verbosity = 0; if (callbacks[i].error === cb) delete callbacks[i].error; } }; -}).call(this); +})(); (function() { "use strict"; @@ -355,7 +370,7 @@ store.verbosity = 0; isReady = false; callbacks = []; }; -}).call(this); +})(); (function() { "use strict"; @@ -365,7 +380,7 @@ store.verbosity = 0; store.order.unregister(callback); store.error.unregister(callback); }; -}).call(this); +})(); (function() { "use strict"; @@ -394,7 +409,7 @@ store.verbosity = 0; store.validator(product, callback); } }; -}).call(this); +})(); (function() { "use strict"; @@ -414,7 +429,7 @@ store.verbosity = 0; } store.trigger("re-refreshed"); }; -}).call(this); +})(); (function() { "use strict"; @@ -443,7 +458,7 @@ store.verbosity = 0; log(store.DEBUG, o); } }; -}).call(this); +})(); (function() { "use strict"; @@ -460,7 +475,7 @@ store.verbosity = 0; this.byAlias = {}; this.byId = {}; }; -}).call(this); +})(); (function() { "use strict"; @@ -496,7 +511,7 @@ store.verbosity = 0; store.Product.prototype.trigger = function(action, args) { store.trigger(this, action, args); }; -}).call(this); +})(); (function() { "use strict"; @@ -551,7 +566,7 @@ store.verbosity = 0; triggerWhenProduct: function(product, action, args) { var queries = []; if (product && product.id) queries.push(product.id + " " + action); - if (product && product.alias && product.alias != product.id) queries.push(product.alias + " " + action); + if (product && product.alias && product.alias !== product.id) queries.push(product.alias + " " + action); if (product && product.type) queries.push(product.type + " " + action); if (product && product.type && (product.type === store.FREE_SUBSCRIPTION || product.type === store.PAID_SUBSCRIPTION)) queries.push("subscription " + action); if (product && product.valid === true) queries.push("valid " + action); @@ -585,7 +600,7 @@ store.verbosity = 0; throw err; }, 1); } -}).call(this); +})(); (function() { "use strict"; @@ -607,7 +622,7 @@ store.verbosity = 0; } store._queries.triggerWhenProduct(product, action, args); }; -}).call(this); +})(); (function() { "use strict"; @@ -639,7 +654,7 @@ store.verbosity = 0; throw err; }, 1); } -}).call(this); +})(); (function() { "use strict"; @@ -654,7 +669,6 @@ store.verbosity = 0; }, callExternal: function(name, callback) { try { - store.log.debug("calling " + name); var args = Array.prototype.slice.call(arguments, 2); if (callback) callback.apply(this, args); } catch (e) { @@ -665,7 +679,7 @@ store.verbosity = 0; var doneCb = function() {}; var xhr = new XMLHttpRequest(); xhr.open(options.method || "POST", options.url, true); - xhr.onreadystatechange = function(event) { + xhr.onreadystatechange = function() { try { if (xhr.readyState === 4) { if (xhr.status === 200) { @@ -696,305 +710,276 @@ store.verbosity = 0; }; } }; -}).call(this); - -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.receiptForTransaction = {}; - this.receiptForProduct = {}; - if (window.localStorage && window.localStorage.sk_receiptForTransaction) this.receiptForTransaction = JSON.parse(window.localStorage.sk_receiptForTransaction); - if (window.localStorage && window.localStorage.sk_receiptForProduct) this.receiptForProduct = JSON.parse(window.localStorage.sk_receiptForProduct); -}; - -var noop = function() {}; - -var log = noop; - -var ERROR_CODES_BASE = 6777e3; - -InAppPurchase.prototype.ERR_SETUP = ERROR_CODES_BASE + 1; - -InAppPurchase.prototype.ERR_LOAD = ERROR_CODES_BASE + 2; - -InAppPurchase.prototype.ERR_PURCHASE = ERROR_CODES_BASE + 3; - -InAppPurchase.prototype.ERR_LOAD_RECEIPTS = ERROR_CODES_BASE + 4; - -InAppPurchase.prototype.ERR_CLIENT_INVALID = ERROR_CODES_BASE + 5; +})(); -InAppPurchase.prototype.ERR_PAYMENT_CANCELLED = ERROR_CODES_BASE + 6; - -InAppPurchase.prototype.ERR_PAYMENT_INVALID = ERROR_CODES_BASE + 7; - -InAppPurchase.prototype.ERR_PAYMENT_NOT_ALLOWED = ERROR_CODES_BASE + 8; - -InAppPurchase.prototype.ERR_UNKNOWN = ERROR_CODES_BASE + 10; - -InAppPurchase.prototype.ERR_REFRESH_RECEIPTS = ERROR_CODES_BASE + 11; - -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, - finish: options.finish || noop, - restore: options.restore || noop, - receiptsRefreshed: options.receiptsRefreshed || noop, - restoreFailed: options.restoreFailed || noop, - restoreCompleted: options.restoreCompleted || noop - }; - if (options.debug) { - exec("debug", [], noop, noop); - log = function(msg) { - console.log("InAppPurchase[js]: " + msg); - }; - } - if (options.noAutoFinish) { - exec("noAutoFinish", [], 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(); - }; - var setupFailed = function() { - log("setup failed"); - protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_SETUP, "Setup failed"); - protectCall(error, "init.error"); +(function() { + "use strict"; + var exec = function(methodName, options, success, error) { + cordova.exec(success, error, "InAppPurchase", methodName, options); }; - exec("setup", [], setupOk, setupFailed); -}; - -InAppPurchase.prototype.purchase = function(productId, quantity) { - quantity = quantity | 0 || 1; - var options = this.options; - 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", InAppPurchase.prototype.ERR_PURCHASE, "Trying to purchase a unknown product.", productId, quantity); + var protectCall = function(callback, context) { + if (!callback) { + return; } - return; - } - var purchaseOk = function() { - log("Purchased " + productId); - if (typeof options.purchaseEnqueued === "function") { - protectCall(options.purchaseEnqueued, "options.purchaseEnqueued", productId, quantity); + try { + var args = Array.prototype.slice.call(arguments, 2); + callback.apply(this, args); + } catch (err) { + log("exception in " + context + ': "' + err + '"'); } }; - var purchaseFailed = function() { - var msg = "Purchasing " + productId + " failed"; - log(msg); - if (typeof options.error === "function") { - protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_PURCHASE, msg, productId, quantity); + var InAppPurchase = function() { + this.options = {}; + this.receiptForTransaction = {}; + this.receiptForProduct = {}; + if (window.localStorage && window.localStorage.sk_receiptForTransaction) this.receiptForTransaction = JSON.parse(window.localStorage.sk_receiptForTransaction); + if (window.localStorage && window.localStorage.sk_receiptForProduct) this.receiptForProduct = JSON.parse(window.localStorage.sk_receiptForProduct); + }; + var noop = function() {}; + var log = noop; + var ERROR_CODES_BASE = 6777e3; + InAppPurchase.prototype.ERR_SETUP = ERROR_CODES_BASE + 1; + InAppPurchase.prototype.ERR_LOAD = ERROR_CODES_BASE + 2; + InAppPurchase.prototype.ERR_PURCHASE = ERROR_CODES_BASE + 3; + InAppPurchase.prototype.ERR_LOAD_RECEIPTS = ERROR_CODES_BASE + 4; + InAppPurchase.prototype.ERR_CLIENT_INVALID = ERROR_CODES_BASE + 5; + InAppPurchase.prototype.ERR_PAYMENT_CANCELLED = ERROR_CODES_BASE + 6; + InAppPurchase.prototype.ERR_PAYMENT_INVALID = ERROR_CODES_BASE + 7; + InAppPurchase.prototype.ERR_PAYMENT_NOT_ALLOWED = ERROR_CODES_BASE + 8; + InAppPurchase.prototype.ERR_UNKNOWN = ERROR_CODES_BASE + 10; + InAppPurchase.prototype.ERR_REFRESH_RECEIPTS = ERROR_CODES_BASE + 11; + 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, + finish: options.finish || noop, + restore: options.restore || noop, + receiptsRefreshed: options.receiptsRefreshed || noop, + restoreFailed: options.restoreFailed || noop, + restoreCompleted: options.restoreCompleted || noop + }; + if (options.debug) { + exec("debug", [], noop, noop); + log = function(msg) { + console.log("InAppPurchase[js]: " + msg); + }; } + if (options.noAutoFinish) { + exec("noAutoFinish", [], 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(); + }; + var setupFailed = function() { + log("setup failed"); + protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_SETUP, "Setup failed"); + protectCall(error, "init.error"); + }; + exec("setup", [], setupOk, setupFailed); }; - return exec("purchase", [ productId, quantity ], purchaseOk, purchaseFailed); -}; - -InAppPurchase.prototype.restore = function() { - this.needRestoreNotification = true; - return exec("restoreCompletedTransactions", []); -}; - -InAppPurchase.prototype.load = function(productIds, success, error) { - var options = this.options; - if (typeof productIds === "string") { - productIds = [ productIds ]; - } - if (!productIds) { - protectCall(success, "load.success", [], []); - } else if (!productIds.length) { - protectCall(success, "load.success", [], []); - } else { - if (typeof productIds[0] !== "string") { - var msg = "invalid productIds given to store.load: " + JSON.stringify(productIds); + InAppPurchase.prototype.purchase = function(productId, quantity) { + quantity = quantity | 0 || 1; + var options = this.options; + if (!InAppPurchase._productIds || InAppPurchase._productIds.indexOf(productId) < 0) { + var msg = "Purchasing " + productId + " failed. Ensure the product was loaded first with storekit.load(...)!"; log(msg); - protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_LOAD, msg); - protectCall(error, "load.error", InAppPurchase.prototype.ERR_LOAD, msg); + if (typeof options.error === "function") { + protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_PURCHASE, "Trying to purchase a unknown product.", productId, quantity); + } 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 purchaseOk = function() { + log("Purchased " + productId); + if (typeof options.purchaseEnqueued === "function") { + protectCall(options.purchaseEnqueued, "options.purchaseEnqueued", productId, quantity); + } }; - var loadFailed = function(errMessage) { - log("load failed"); - log(errMessage); - var message = "Load failed: " + errMessage; - protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_LOAD, message); - protectCall(error, "load.error", InAppPurchase.prototype.ERR_LOAD, message); + var purchaseFailed = function() { + var errmsg = "Purchasing " + productId + " failed"; + log(errmsg); + if (typeof options.error === "function") { + protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_PURCHASE, errmsg, productId, quantity); + } }; - InAppPurchase._productIds = productIds; - exec("load", [ productIds ], loadOk, loadFailed); - } -}; - -InAppPurchase.prototype.finish = function(transactionId) { - exec("finishTransaction", [ transactionId ], noop, noop); -}; - -var pendingUpdates = []; - -InAppPurchase.prototype.processPendingUpdates = function() { - for (var i = 0; i < pendingUpdates.length; ++i) { - this.updatedTransactionCallback.apply(this, pendingUpdates[i]); - } - pendingUpdates = []; -}; - -InAppPurchase.prototype.updatedTransactionCallback = function(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt) { - if (!initialized) { - var args = Array.prototype.slice.call(arguments); - pendingUpdates.push(args); - return; - } - if (transactionReceipt) { - this.receiptForProduct[productId] = transactionReceipt; - this.receiptForTransaction[transactionIdentifier] = transactionReceipt; - if (window.localStorage) { - window.localStorage.sk_receiptForProduct = JSON.stringify(this.receiptForProduct); - window.localStorage.sk_receiptForTransaction = JSON.stringify(this.receiptForTransaction); + exec("purchase", [ productId, quantity ], purchaseOk, purchaseFailed); + }; + InAppPurchase.prototype.restore = function() { + this.needRestoreNotification = true; + exec("restoreCompletedTransactions", []); + }; + InAppPurchase.prototype.load = function(productIds, success, error) { + var options = this.options; + if (typeof productIds === "string") { + productIds = [ productIds ]; } - } - switch (state) { - case "PaymentTransactionStatePurchasing": - protectCall(this.options.purchasing, "options.purchasing", productId); - return; - - case "PaymentTransactionStatePurchased": - protectCall(this.options.purchase, "options.purchase", transactionIdentifier, 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; + if (!productIds) { + protectCall(success, "load.success", [], []); + } else if (!productIds.length) { + 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", InAppPurchase.prototype.ERR_LOAD, msg); + protectCall(error, "load.error", InAppPurchase.prototype.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", InAppPurchase.prototype.ERR_LOAD, message); + protectCall(error, "load.error", InAppPurchase.prototype.ERR_LOAD, message); + }; + InAppPurchase._productIds = productIds; + exec("load", [ productIds ], loadOk, loadFailed); + } + }; + InAppPurchase.prototype.finish = function(transactionId) { + exec("finishTransaction", [ transactionId ], noop, noop); + }; + var pendingUpdates = []; + InAppPurchase.prototype.processPendingUpdates = function() { + for (var i = 0; i < pendingUpdates.length; ++i) { + this.updatedTransactionCallback.apply(this, pendingUpdates[i]); + } + pendingUpdates = []; + }; + InAppPurchase.prototype.updatedTransactionCallback = function(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt) { + if (!initialized) { + var args = Array.prototype.slice.call(arguments); + pendingUpdates.push(args); + return; + } + if (transactionReceipt) { + this.receiptForProduct[productId] = transactionReceipt; + this.receiptForTransaction[transactionIdentifier] = transactionReceipt; + if (window.localStorage) { + window.localStorage.sk_receiptForProduct = JSON.stringify(this.receiptForProduct); + window.localStorage.sk_receiptForTransaction = JSON.stringify(this.receiptForTransaction); + } + } + switch (state) { + case "PaymentTransactionStatePurchasing": + protectCall(this.options.purchasing, "options.purchasing", productId); + return; - case "PaymentTransactionStateFinished": - protectCall(this.options.finish, "options.finish", transactionIdentifier, productId); - return; - } -}; + case "PaymentTransactionStatePurchased": + protectCall(this.options.purchase, "options.purchase", transactionIdentifier, productId); + return; -InAppPurchase.prototype.restoreCompletedTransactionsFinished = function() { - if (this.needRestoreNotification) delete this.needRestoreNotification; else return; - protectCall(this.options.restoreCompleted, "options.restoreCompleted"); -}; + case "PaymentTransactionStateFailed": + protectCall(this.options.error, "options.error", errorCode, errorText, { + productId: productId + }); + return; -InAppPurchase.prototype.restoreCompletedTransactionsFailed = function(errorCode) { - if (this.needRestoreNotification) delete this.needRestoreNotification; else return; - protectCall(this.options.restoreFailed, "options.restoreFailed", errorCode); -}; + case "PaymentTransactionStateRestored": + protectCall(this.options.restore, "options.restore", transactionIdentifier, productId); + return; -InAppPurchase.prototype.refreshReceipts = function() { - var that = this; - that.appStoreReceipt = null; - var loaded = function(base64) { - that.appStoreReceipt = base64; - protectCall(that.options.receiptsRefreshed, "options.receiptsRefreshed", base64); + case "PaymentTransactionStateFinished": + protectCall(this.options.finish, "options.finish", transactionIdentifier, productId); + return; + } }; - var error = function(errMessage) { - log("refresh receipt failed: " + errMessage); - protectcall(options.error, "options.error", InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, "Failed to refresh receipt: " + errMessage); + InAppPurchase.prototype.restoreCompletedTransactionsFinished = function() { + if (this.needRestoreNotification) delete this.needRestoreNotification; else return; + protectCall(this.options.restoreCompleted, "options.restoreCompleted"); }; - exec("appStoreRefreshReceipt", [], loaded, error); -}; - -InAppPurchase.prototype.loadReceipts = function(callback) { - var that = this; - that.appStoreReceipt = null; - var loaded = function(base64) { - that.appStoreReceipt = base64; - callCallback(); + InAppPurchase.prototype.restoreCompletedTransactionsFailed = function(errorCode) { + if (this.needRestoreNotification) delete this.needRestoreNotification; else return; + protectCall(this.options.restoreFailed, "options.restoreFailed", errorCode); }; - var error = function(errMessage) { - log("load failed: " + errMessage); - protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_LOAD_RECEIPTS, "Failed to load receipt: " + errMessage); + InAppPurchase.prototype.refreshReceipts = function() { + var that = this; + that.appStoreReceipt = null; + var loaded = function(base64) { + that.appStoreReceipt = base64; + protectCall(that.options.receiptsRefreshed, "options.receiptsRefreshed", base64); + }; + var error = function(errMessage) { + log("refresh receipt failed: " + errMessage); + protectCall(that.options.error, "options.error", InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, "Failed to refresh receipt: " + errMessage); + }; + exec("appStoreRefreshReceipt", [], loaded, error); }; - var callCallback = function() { - if (callback) { - protectCall(callback, "loadReceipts.callback", { - appStoreReceipt: that.appStoreReceipt, - forTransaction: function(transactionId) { - return that.receiptForTransaction[transactionId] || null; - }, - forProduct: function(productId) { - return that.receiptForProduct[productId] || null; - } - }); + InAppPurchase.prototype.loadReceipts = function(callback) { + var that = this; + that.appStoreReceipt = null; + var loaded = function(base64) { + that.appStoreReceipt = base64; + callCallback(); + }; + var error = function(errMessage) { + log("load failed: " + errMessage); + protectCall(that.options.error, "options.error", InAppPurchase.prototype.ERR_LOAD_RECEIPTS, "Failed to load receipt: " + errMessage); + }; + function callCallback() { + if (callback) { + protectCall(callback, "loadReceipts.callback", { + appStoreReceipt: that.appStoreReceipt, + forTransaction: function(transactionId) { + return that.receiptForTransaction[transactionId] || null; + }, + forProduct: function(productId) { + return that.receiptForProduct[productId] || null; + } + }); + } } + exec("appStoreReceipt", [], loaded, error); }; - exec("appStoreReceipt", [], loaded, error); -}; - -InAppPurchase.prototype.runQueue = function() { - if (!this.eventQueue.length || !this.onPurchased && !this.onFailed && !this.onRestored) { - return; - } - var args; - var queue = this.eventQueue.slice(); - this.eventQueue = []; - args = queue.shift(); - while (args) { - this.updatedTransactionCallback.apply(this, args); + InAppPurchase.prototype.runQueue = function() { + if (!this.eventQueue.length || !this.onPurchased && !this.onFailed && !this.onRestored) { + return; + } + var args; + var queue = this.eventQueue.slice(); + this.eventQueue = []; 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(); - }, 1e4); -}; - -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(); + 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(); + }, 1e4); + }; + 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(); +})(); (function() { "use strict"; @@ -1055,7 +1040,7 @@ window.storekit = new InAppPurchase(); }); var initialized = false; var initializing = false; - var storekitInit = function() { + function storekitInit() { if (initialized || initializing) return; initializing = true; store.log.debug("ios -> initializing storekit"); @@ -1069,21 +1054,21 @@ window.storekit = new InAppPurchase(); restoreCompleted: storekitRestoreCompleted, restoreFailed: storekitRestoreFailed }, storekitReady, storekitInitFailed); - }; - var storekitReady = function() { + } + function storekitReady() { store.log.info("ios -> storekit ready"); initializing = false; initialized = true; storekitLoad(); - }; - var storekitInitFailed = function() { + } + function storekitInitFailed() { store.log.warn("ios -> storekit init failed"); initializing = false; retry(storekitInit); - }; + } var loaded = false; var loading = false; - var storekitLoad = function() { + function storekitLoad() { if (!initialized) return; if (loaded || loading) return; loading = true; @@ -1091,8 +1076,8 @@ window.storekit = new InAppPurchase(); for (var i = 0; i < store.products.length; ++i) products.push(store.products[i].id); store.log.debug("ios -> loading products"); storekit.load(products, storekitLoaded, storekitLoadFailed); - }; - var storekitLoaded = function(validProducts, invalidProductIds) { + } + function storekitLoaded(validProducts, invalidProductIds) { store.log.debug("ios -> products loaded"); var p; for (var i = 0; i < validProducts.length; ++i) { @@ -1121,13 +1106,13 @@ window.storekit = new InAppPurchase(); storekit.loaded = true; store.ready(true); }, 1); - }; - var storekitLoadFailed = function() { + } + function storekitLoadFailed() { store.log.warn("ios -> loading products failed"); loading = false; retry(storekitLoad); - }; - var storekitPurchasing = function(productId) { + } + function storekitPurchasing(productId) { store.log.debug("ios -> is purchasing " + productId); store.ready(function() { var product = store.get(productId); @@ -1137,8 +1122,8 @@ window.storekit = new InAppPurchase(); } if (product.state !== store.INITIATED) product.set("state", store.INITIATED); }); - }; - var storekitPurchased = function(transactionId, productId) { + } + function storekitPurchased(transactionId, productId) { store.ready(function() { var product = store.get(productId); if (!product) { @@ -1162,8 +1147,8 @@ window.storekit = new InAppPurchase(); store.log.info("ios -> transaction " + transactionId + " purchased (" + product.transactions.length + " in the queue for " + productId + ")"); product.set("state", store.APPROVED); }); - }; - var storekitError = function(errorCode, errorText, options) { + } + function storekitError(errorCode, errorText, options) { var i, p; if (!options) options = {}; store.log.error("ios -> ERROR " + errorCode + ": " + errorText + " - " + JSON.stringify(options)); @@ -1191,7 +1176,7 @@ window.storekit = new InAppPurchase(); code: errorCode, message: errorText }); - }; + } store.when("re-refreshed", function() { storekit.restore(); }); @@ -1202,7 +1187,7 @@ window.storekit = new InAppPurchase(); function storekitRestoreCompleted() { store.log.info("ios -> restore completed"); } - function storekitRestoreFailed(errorCode) { + function storekitRestoreFailed() { store.log.warn("ios -> restore failed"); store.error({ code: store.ERR_REFRESH, @@ -1252,6 +1237,6 @@ window.storekit = new InAppPurchase(); a[i].fn.call(this); } }, false); -}).call(this); +})(); module.exports = store; \ No newline at end of file From aaef1b94744e2ecd026cad57cd0be11c07a4906d Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Fri, 31 Oct 2014 01:06:18 +0200 Subject: [PATCH 02/11] Update AndroidIAP --- git_modules/android_iap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git_modules/android_iap b/git_modules/android_iap index 91885090..da554c5e 160000 --- a/git_modules/android_iap +++ b/git_modules/android_iap @@ -1 +1 @@ -Subproject commit 918850907983a8a575934a47f0ddaa31687ddc0f +Subproject commit da554c5ebba8f0a44eb671362a0882aabe9d2ae8 From 1f04a9b7bea9c586e68f4d75fbaa718482332cbf Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Fri, 31 Oct 2014 01:11:36 +0200 Subject: [PATCH 03/11] Check that Android error is a string --- git_modules/android_iap | 2 +- src/js/platforms/android-bridge.js | 2 +- www/store-android.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/git_modules/android_iap b/git_modules/android_iap index da554c5e..e2273e0a 160000 --- a/git_modules/android_iap +++ b/git_modules/android_iap @@ -1 +1 @@ -Subproject commit da554c5ebba8f0a44eb671362a0882aabe9d2ae8 +Subproject commit e2273e0aafb342bbb0dedd3a840d852a52e037f2 diff --git a/src/js/platforms/android-bridge.js b/src/js/platforms/android-bridge.js index 1d18d4aa..cde090ec 100644 --- a/src/js/platforms/android-bridge.js +++ b/src/js/platforms/android-bridge.js @@ -124,7 +124,7 @@ function errorCb(fail) { return function(error) { if (!fail) return; - var tokens = error.split('|'); + var tokens = typeof error === 'string' ? error.split('|') : [ error ]; if (tokens.length > 1 && /^[-+]?(\d+)$/.test(tokens[0])) { var code = tokens[0]; var message = tokens[1]; diff --git a/www/store-android.js b/www/store-android.js index 55924677..12c3728c 100644 --- a/www/store-android.js +++ b/www/store-android.js @@ -809,7 +809,7 @@ store.verbosity = 0; function errorCb(fail) { return function(error) { if (!fail) return; - var tokens = error.split("|"); + var tokens = typeof error === "string" ? error.split("|") : [ error ]; if (tokens.length > 1 && /^[-+]?(\d+)$/.test(tokens[0])) { var code = tokens[0]; var message = tokens[1]; From 939aefd6ec10f24ccd4ef0ed9ca358f12767ad39 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Fri, 7 Nov 2014 09:02:55 +0200 Subject: [PATCH 04/11] Fix generation of contributor documentation --- Makefile | 2 +- doc/contributor-guide.md | 101 ++++++++++++++++++++++++++++++++++++++- src/js/store-ios.js | 2 +- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 19894f59..18825497 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ doc-contrib: test-js @echo >> doc/contributor-guide.md @echo "*(generated from source files using \`make doc-contrib)\`*" >> doc/contributor-guide.md @echo >> doc/contributor-guide.md - @cat src/js/*.js | grep "//!" | cut -d! -f2- | cut -d\ -f2- >> doc/contributor-guide.md + @cat src/js/*.js src/js/platforms/*.js | grep "//!" | cut -d! -f2- | cut -d\ -f2- >> doc/contributor-guide.md doc: doc-api doc-contrib diff --git a/doc/contributor-guide.md b/doc/contributor-guide.md index 3aaaa94e..7065d526 100644 --- a/doc/contributor-guide.md +++ b/doc/contributor-guide.md @@ -7,7 +7,7 @@ The implementation of the unified API is a small layer built on top of the legacy "PhoneGap-InAppPurchase-iOS" plugin. -This was first decided as a temporary "get-things"done" solution. +This was first decided as a temporary "get-things-done" solution. However, I found this ended-up providing a nice separation of concerns: - the `platforms/ios-bridge.js` file exposes an API called `storekit` that matches the @@ -22,3 +22,102 @@ However, I found this ended-up providing a nice separation of concerns: - It reacts to product's changes of state, so that a product get's purchased when `REQUESTED`, or finished when `FINISHED` for instance. +## Reacting to product state changes + +The iOS implementation monitors products changes of state to trigger +`storekit` operations. + +Please refer to the [product life-cycle section](api.md#life-cycle) of the documentation +for better understanding of the job of this event handlers. +#### initialize storekit +At first refresh, initialize the storekit API. See [`storekitInit()`](#storekitInit) for details. + +#### initiate a purchase + +When a product enters the store.REQUESTED state, initiate a purchase with `storekit`. + +#### finish a purchase +When a product enters the store.FINISHED state, `finish()` the storekit transaction. + +#### persist ownership + +`storekit` doesn't provide a way to know which products have been purchases. +That is why we have to handle that ourselves, by storing the `OWNED` status of a product. + +Note that, until Apple provides a mean to get notified to refunds, there's no way back. +A non-consumable product, once `OWNED` always will be. + +http://stackoverflow.com/questions/6429186/can-we-check-if-a-users-in-app-purchase-has-been-refunded-by-apple + + +## Initialization + +### *storekitInit()* + +This funciton will initialize the storekit API. + +This initiates a chain reaction including [`storekitReady()`](#storekitReady) and [`storekitLoaded()`](#storekitLoaded) +that will make sure products are loaded from server, set as `VALID` or `INVALID`, and eventually restored +to their proper `OWNED` status. + +It also registers the `storekit` callbacks to get notified of events from the StoreKit API: + + - [`storekitPurchasing()`](#storekitPurchasing) + - [`storekitPurchased()`](#storekitPurchased) + - [`storekitError()`](#storekitError) + + +## *storekit* events handlers + +### *storekitReady()* + +Called when `storekit` has been initialized successfully. + +Loads all registered products, triggers `storekitLoaded()` when done. + +### *storekitLoaded()* + +Update the `store`'s product definitions when they have been loaded. + + 1. Set the products state to `VALID` or `INVALID` + 2. Trigger the "loaded" event + 3. Set the products state to `OWNED` (if it is so) + 4. Set the store status to "ready". + +Note: the execution of "ready" is deferred to make sure state +changes have been processed. +### *storekitPurchasing()* + +Called by `storekit` when a purchase is in progress. + +It will set the product state to `INITIATED`. + +### *storekitPurchased()* + +Called by `storekit` when a purchase have been approved. + +It will set the product state to `APPROVED` and associates the product +with the order's transaction identifier. + +### *storekitError()* + +Called by `storekit` when an error happens in the storekit API. + +Will convert storekit errors to a [`store.Error`](api.md/#errors). + + +## Persistance of the *OWNED* status + +#### *isOwned(productId)* +return true iff the product with given ID has been purchased and finished +during this or a previous execution of the application. +#### *setOwned(productId, value)* +store the boolean OWNED status of a given product. + +## Retry failed requests +When setup and/or load failed, the plugin will retry over and over till it can connect +to the store. + +However, to be nice with the battery, it'll double the retry timeout each time. + +Special case, when the device goes online, it'll trigger all retry callback in the queue. diff --git a/src/js/store-ios.js b/src/js/store-ios.js index 284efa12..d5c15f8e 100644 --- a/src/js/store-ios.js +++ b/src/js/store-ios.js @@ -3,7 +3,7 @@ //! The implementation of the unified API is a small layer //! built on top of the legacy "PhoneGap-InAppPurchase-iOS" plugin. //! -//! This was first decided as a temporary "get-things"done" solution. +//! This was first decided as a temporary "get-things-done" solution. //! However, I found this ended-up providing a nice separation of concerns: //! //! - the `platforms/ios-bridge.js` file exposes an API called `storekit` that matches the From 605a0c5b82e176041ae5f5569448dee831aee553 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Fri, 7 Nov 2014 09:13:31 +0200 Subject: [PATCH 05/11] Add doc --- doc/api.md | 2 +- src/js/product.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api.md b/doc/api.md index 1969c9a3..bf1d3deb 100644 --- a/doc/api.md +++ b/doc/api.md @@ -415,7 +415,7 @@ Find below a diagram of the different states a product can pass by. - `REQUESTED`: order (purchase) requested by the user - `INITIATED`: order transmitted to the server - `APPROVED`: purchase approved by server - - `FINISHED`: purchase delivered by the app + - `FINISHED`: purchase delivered by the app (see [Finish a Purchase](#finish-a-purchase)) - `OWNED`: purchase is owned (only for non-consumable and subscriptions) #### Notes diff --git a/src/js/product.js b/src/js/product.js index 556f42de..1a27db90 100644 --- a/src/js/product.js +++ b/src/js/product.js @@ -256,7 +256,7 @@ store.Product.prototype.verify = function() { /// - `REQUESTED`: order (purchase) requested by the user /// - `INITIATED`: order transmitted to the server /// - `APPROVED`: purchase approved by server -/// - `FINISHED`: purchase delivered by the app +/// - `FINISHED`: purchase delivered by the app (see [Finish a Purchase](#finish-a-purchase)) /// - `OWNED`: purchase is owned (only for non-consumable and subscriptions) /// /// #### Notes From 22aef6f13c8fa7543a8c7883ca9e709ac82aa7ed Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Fri, 7 Nov 2014 09:39:12 +0200 Subject: [PATCH 06/11] Prevent finishTransaction on "Purchasing" ones --- src/ios/InAppPurchase.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ios/InAppPurchase.m b/src/ios/InAppPurchase.m index 3240f8ab..b709b098 100644 --- a/src/ios/InAppPurchase.m +++ b/src/ios/InAppPurchase.m @@ -380,6 +380,7 @@ - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transa error = state = transactionIdentifier = transactionReceipt = productId = @""; errorCode = 0; DLog(@"Transaction updated: %@", transaction.payment.productIdentifier); + BOOL canFinish = NO; switch (transaction.transactionState) { @@ -394,6 +395,7 @@ - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transa transactionIdentifier = transaction.transactionIdentifier; transactionReceipt = [[transaction transactionReceipt] base64EncodedString]; productId = transaction.payment.productIdentifier; + canFinish = YES; break; case SKPaymentTransactionStateFailed: @@ -401,8 +403,9 @@ - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transa error = transaction.error.localizedDescription; errorCode = jsErrorCode(transaction.error.code); productId = transaction.payment.productIdentifier; + canFinish = YES; DLog(@"Error %@ - %@", jsErrorCodeAsString(errorCode), error); - + // Finish failed transactions, when autoFinish is off if (!g_autoFinishEnabled) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; @@ -417,6 +420,7 @@ - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transa transactionIdentifier = transaction.originalTransaction.transactionIdentifier; transactionReceipt = [[transaction transactionReceipt] base64EncodedString]; productId = transaction.originalTransaction.payment.productIdentifier; + canFinish = YES; break; default: @@ -437,7 +441,7 @@ - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transa [callbackArgs JSONSerialize]]; // DLog(@"js: %@", js); [self.commandDelegate evalJs:js]; - if (g_autoFinishEnabled) { + if (g_autoFinishEnabled && canFinish) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; [self transactionFinished:transaction]; } @@ -557,7 +561,7 @@ - (void) verifyReceipt: (CDVInvokedUrlCommand*)command { // Verify the signature BIO *b_receiptPayload; int result = PKCS7_verify(p7, NULL, store, b_receiptPayload, 0); - + free(receiptBytes); free(appleBytes); From d4e59c080627464a89c914d251688ca5e74a9fe8 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Tue, 11 Nov 2014 11:13:59 +0200 Subject: [PATCH 07/11] Update misleading documentation --- doc/android.md | 39 ++------------------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/doc/android.md b/doc/android.md index af52ea62..b3ed5848 100644 --- a/doc/android.md +++ b/doc/android.md @@ -1,38 +1,3 @@ -# Setup - -## Add your license key - -As android uses a public key for verification of purchase data consistency, you have to put the public key of your application inside the config.xml file under a preference tag like this: - - - -You can find your public key inside Google Developer Console under SERVICES & APIS tab of the specific application. - -## PlayStore - -from https://github.com/mohamnag/InAppBilling/wiki/Stores%20setup#playstore - -> Recently you can test IAP on PlayStore only if your app is already published. If you don't want to make it available before testing, you can publish it in alpha (recommended) or beta state instead of production. - -Read Google's documentations about IAP and testing it. Any thing may have been changed in between. - -Steps to setup IAP in Google's developer console: - -1. `Developer Console` Create your app, provide all needed meta data - -2. `Developer Console` Upload a **signed release** APK to either alpha, beta or production - -3. `Developer Console` Setup your in app items - -4. `Developer Console` Setup your test users if you don't want to spend real money buying your own app ;) -Its easier to be done with an alpha published app. You add users to a google group then send a link and they sign up on their own. - -4. `Developer Console` Publish your app and wait until it is available to (test) users. -This may take hours! - -5. Install **same** release signed app on a device which is logged into play store using a test account. -If you use the debug build, you can do some actions, however you will not be able to buy the product. - -6. Try in-app purchasing! - +This have moved: +See https://github.com/j3k0/cordova-plugin-purchase/wiki/HOWTO#setup-android-applications From df492ae2ccdafb1c08bd76461a54711f0cac1e7a Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Wed, 12 Nov 2014 10:38:50 +0200 Subject: [PATCH 08/11] Refresh receipts & Add info about app --- .gitignore | 2 + Makefile | 3 +- src/ios/InAppPurchase.h | 11 +- src/ios/InAppPurchase.m | 129 +++++++++++++++++++----- src/js/platforms/ios-adapter.js | 98 +++++++++++++++--- src/js/platforms/ios-bridge.js | 74 ++++++++++---- src/js/product.js | 20 ++-- test/js/test-ios.js | 17 +++- www/store-android.js | 19 ++-- www/store-ios.js | 172 +++++++++++++++++++++++++------- 10 files changed, 433 insertions(+), 112 deletions(-) diff --git a/.gitignore b/.gitignore index 7689f351..c92c216d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /node_modules/ /coverage/ /test/tmp/ +/www/.store-android.js +/www/.store-ios.js *~ diff --git a/Makefile b/Makefile index 18825497..179be00c 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ build: sync-android test-js @echo "- Preprocess" @node_modules/.bin/preprocess src/js/store-ios.js src/js | node_modules/.bin/uglifyjs -b > www/store-ios.js @node_modules/.bin/preprocess src/js/store-android.js src/js | node_modules/.bin/uglifyjs -b > www/store-android.js - @echo "- DONE" + @echo "- Done" @echo "" prepare-test-js: @@ -44,6 +44,7 @@ eslint: jshint test-js: jshint eslint prepare-test-js @echo "- Mocha" @node_modules/.bin/istanbul test --root test/tmp test/js/run.js + @echo test-js-coverage: jshint eslint prepare-test-js @echo "- Mocha / Instanbul" diff --git a/src/ios/InAppPurchase.h b/src/ios/InAppPurchase.h index 621b88e3..7262e653 100644 --- a/src/ios/InAppPurchase.h +++ b/src/ios/InAppPurchase.h @@ -26,6 +26,7 @@ - (void) load: (CDVInvokedUrlCommand*)command; - (void) purchase: (CDVInvokedUrlCommand*)command; - (void) appStoreReceipt: (CDVInvokedUrlCommand*)command; +- (void) appStoreRefreshReceipt: (CDVInvokedUrlCommand*)command; - (void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions; - (void) paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error; @@ -44,5 +45,13 @@ @property (nonatomic,retain) InAppPurchase* plugin; @property (nonatomic,retain) CDVInvokedUrlCommand* command; - @end; + +@interface RefreshReceiptDelegate : NSObject { + InAppPurchase* plugin; + CDVInvokedUrlCommand* command; +} + +@property (nonatomic,retain) InAppPurchase* plugin; +@property (nonatomic,retain) CDVInvokedUrlCommand* command; +@end diff --git a/src/ios/InAppPurchase.m b/src/ios/InAppPurchase.m index b709b098..b17915f2 100644 --- a/src/ios/InAppPurchase.m +++ b/src/ios/InAppPurchase.m @@ -32,6 +32,7 @@ #define ERR_PAYMENT_INVALID (ERROR_CODES_BASE + 7) #define ERR_PAYMENT_NOT_ALLOWED (ERROR_CODES_BASE + 8) #define ERR_UNKNOWN (ERROR_CODES_BASE + 10) +#define ERR_REFRESH_RECEIPTS (ERROR_CODES_BASE + 11) static NSInteger jsErrorCode(NSInteger storeKitErrorCode) { @@ -56,6 +57,7 @@ static NSInteger jsErrorCode(NSInteger storeKitErrorCode) case ERR_LOAD: return @"ERR_LOAD"; case ERR_PURCHASE: return @"ERR_PURCHASE"; case ERR_LOAD_RECEIPTS: return @"ERR_LOAD_RECEIPTS"; + case ERR_REFRESH_RECEIPTS: return @"ERR_REFRESH_RECEIPTS"; case ERR_CLIENT_INVALID: return @"ERR_CLIENT_INVALID"; case ERR_PAYMENT_CANCELLED: return @"ERR_PAYMENT_CANCELLED"; case ERR_PAYMENT_INVALID: return @"ERR_PAYMENT_INVALID"; @@ -71,32 +73,32 @@ static NSInteger jsErrorCode(NSInteger storeKitErrorCode) // maps A=>0,B=>1.. const static unsigned char unb64[]={ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //10 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //20 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //30 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //40 - 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, //50 - 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, //60 - 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, //70 - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, //80 - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, //90 - 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, //100 - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, //110 - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, //120 - 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, //130 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //140 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //150 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //160 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //170 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //180 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //190 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //200 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //210 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //220 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //230 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //240 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //250 - 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //10 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //30 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //40 + 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, //50 + 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, //60 + 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, //70 + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, //80 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, //90 + 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, //100 + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, //110 + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, //120 + 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, //130 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //140 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //150 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //160 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //170 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //180 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //190 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //200 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //210 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //220 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //230 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //240 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //250 + 0, 0, 0, 0, 0, 0, }; // This array has 255 elements // Converts binary data of length=len to base64 characters. @@ -595,6 +597,26 @@ - (void) appStoreReceipt: (CDVInvokedUrlCommand*)command { [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } +- (void) appStoreRefreshReceipt: (CDVInvokedUrlCommand*)command { + DLog(@"Request to refresh app receipt"); + RefreshReceiptDelegate* delegate = [[RefreshReceiptDelegate alloc] init]; + SKReceiptRefreshRequest* recreq = [[SKReceiptRefreshRequest alloc] init]; + recreq.delegate = delegate; + delegate.plugin = self; + delegate.command = command; + +#if ARC_ENABLED + self.retainer[@"receiptRefreshRequest"] = recreq; + self.retainer[@"receiptRefreshRequestDelegate"] = delegate; +#else + [delegate retain]; +#endif + + DLog(@"Starting receipt refresh request..."); + [recreq start]; + DLog(@"Receipt refresh request started"); +} + - (void) dispose { self.retainer = nil; self.list = nil; @@ -605,6 +627,61 @@ - (void) dispose { [super dispose]; } +@end +/** + * Receive refreshed app receipt + */ +@implementation RefreshReceiptDelegate + +@synthesize plugin, command; + +- (void) requestDidFinish:(SKRequest *)request { + DLog(@"Got refreshed receipt"); + NSString *base64 = nil; + NSData *receiptData = [self.plugin appStoreReceipt]; + if (receiptData != nil) { + base64 = [receiptData convertToBase64]; + // DLog(@"base64 receipt: %@", base64); + } + NSBundle *bundle = [NSBundle mainBundle]; + NSArray *callbackArgs = [NSArray arrayWithObjects: + NILABLE(base64), + NILABLE([bundle.infoDictionary objectForKey:@"CFBundleIdentifier"]), + NILABLE([bundle.infoDictionary objectForKey:@"CFBundleShortVersionString"]), + NILABLE([bundle.infoDictionary objectForKey:@"CFBundleNumericVersion"]), + NILABLE([bundle.infoDictionary objectForKey:@"CFBundleSignature"]), + nil]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsArray:callbackArgs]; + DLog(@"Send new receipt data"); + [self.plugin.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + +#if ARC_ENABLED + [self.plugin.retainer removeObjectForKey:@"receiptRefreshRequest"]; + [self.plugin.retainer removeObjectForKey:@"receiptRefreshRequestDelegate"]; +#else + [request release]; + [self release]; +#endif +} + +- (void):(SKRequest *)request didFailWithError:(NSError*) error { + DLog(@"In-App Store unavailable (ERROR %li)", (unsigned long)error.code); + DLog(@"%@", [error localizedDescription]); + + CDVPluginResult* pluginResult = + [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]]; + [self.plugin.commandDelegate sendPluginResult:pluginResult callbackId:self.command.callbackId]; +} + +#if ARC_DISABLED +- (void) dealloc { + [plugin release]; + [command release]; + [super dealloc]; +} +#endif + @end /** diff --git a/src/js/platforms/ios-adapter.js b/src/js/platforms/ios-adapter.js index 630782d3..614e1b10 100644 --- a/src/js/platforms/ios-adapter.js +++ b/src/js/platforms/ios-adapter.js @@ -212,8 +212,8 @@ function storekitLoaded(validProducts, invalidProductIds) { //! Note: the execution of "ready" is deferred to make sure state //! changes have been processed. setTimeout(function() { - storekit.loading = false; - storekit.loaded = true; + loading = false; + loaded = true; store.ready(true); }, 1); } @@ -224,6 +224,38 @@ function storekitLoadFailed() { retry(storekitLoad); } +var refreshCallbacks = []; +var refreshing = false; +function storekitRefreshReceipts(callback) { + if (callback) + refreshCallbacks.push(callback); + if (refreshing) + return; + refreshing = true; + + function callCallbacks() { + var callbacks = refreshCallbacks; + refreshCallbacks = []; + for (var i = 0; i < callbacks.length; ++i) + callbacks[i](); + } + + storekit.refreshReceipts(function() { + // success + refreshing = false; + callCallbacks(); + }, + function() { + // error + refreshing = false; + callCallbacks(); + }); +} + +store.when("expired", function() { + storekitRefreshReceipts(); +}); + //! ### *storekitPurchasing()* //! //! Called by `storekit` when a purchase is in progress. @@ -336,6 +368,27 @@ function storekitError(errorCode, errorText, options) { // }; store.when("re-refreshed", function() { storekit.restore(); + storekit.refreshReceipts(function(data) { + if (data) { + var p = data.bundleIdentifier ? store.get(data.bundleIdentifier) : null; + if (!p) { + p = new store.Product({ + id: data.bundleIdentifier || "application data", + alias: "application data", + type: store.NON_CONSUMABLE + }); + store.register(p); + } + p.version = data.bundleShortVersion; + p.transaction = { + type: 'ios-appstore', + appStoreReceipt: data.appStoreReceipt, + signature: data.signature + }; + p.trigger("loaded"); + p.set('state', store.APPROVED); + } + }); }); function storekitRestored(originalTransactionId, productId) { @@ -355,19 +408,38 @@ function storekitRestoreFailed(/*errorCode*/) { }); } +store._refreshForValidation = function(callback) { + storekitRefreshReceipts(callback); +}; + // Load receipts required by server-side validation of purchases. store._prepareForValidation = function(product, callback) { - storekit.loadReceipts(function(r) { - if (!product.transaction) { - product.transaction = { - type: 'ios-appstore' - }; - } - product.transaction.appStoreReceipt = r.appStoreReceipt; - if (product.transaction.id) - product.transaction.transactionReceipt = r.forTransaction(product.transaction.id); - callback(); - }); + var nRetry = 0; + function loadReceipts() { + storekit.loadReceipts(function(r) { + if (!product.transaction) { + product.transaction = { + type: 'ios-appstore' + }; + } + product.transaction.appStoreReceipt = r.appStoreReceipt; + if (product.transaction.id) + product.transaction.transactionReceipt = r.forTransaction(product.transaction.id); + if (!product.transaction.appStoreReceipt && !product.transaction.transactionReceipt) { + nRetry ++; + if (nRetry < 2) { + setTimeout(loadReceipts, 500); + return; + } + else if (nRetry === 2) { + storekit.refreshReceipts(loadReceipts); + return; + } + } + callback(); + }); + } + loadReceipts(); }; //! diff --git a/src/js/platforms/ios-bridge.js b/src/js/platforms/ios-bridge.js index ea286dc6..2a559ebe 100644 --- a/src/js/platforms/ios-bridge.js +++ b/src/js/platforms/ios-bridge.js @@ -102,6 +102,7 @@ InAppPurchase.prototype.init = function (options, success, error) { protectCall(error, 'init.error'); }; + this.loadAppStoreReceipt(); exec('setup', [], setupOk, setupFailed); }; @@ -296,30 +297,45 @@ InAppPurchase.prototype.restoreCompletedTransactionsFailed = function (errorCode protectCall(this.options.restoreFailed, 'options.restoreFailed', errorCode); }; -InAppPurchase.prototype.refreshReceipts = function() { +InAppPurchase.prototype.refreshReceipts = function(successCb, errorCb) { var that = this; - that.appStoreReceipt = null; - var loaded = function (base64) { - that.appStoreReceipt = base64; - protectCall(that.options.receiptsRefreshed, 'options.receiptsRefreshed', base64); + var loaded = function (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); + that.setAppStoreReceipt(base64); + protectCall(that.options.receiptsRefreshed, 'options.receiptsRefreshed', { + appStoreReceipt: base64, + bundleIdentifier: bundleIdentifier, + bundleShortVersion: bundleShortVersion, + bundleNumericVersion: bundleNumericVersion, + bundleSignature: bundleSignature + }); + protectCall(successCb, "refreshReceipts.success", base64); }; var error = function(errMessage) { log('refresh receipt failed: ' + errMessage); protectCall(that.options.error, 'options.error', InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); + protectCall(errorCb, "refreshReceipts.error", InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); }; + log('refreshing appStoreReceipt'); exec('appStoreRefreshReceipt', [], loaded, error); }; InAppPurchase.prototype.loadReceipts = function (callback) { var that = this; - that.appStoreReceipt = null; + // that.appStoreReceipt = null; var loaded = function (base64) { - that.appStoreReceipt = base64; + // that.appStoreReceipt = base64; + that.setAppStoreReceipt(base64); callCallback(); }; @@ -329,20 +345,40 @@ InAppPurchase.prototype.loadReceipts = function (callback) { }; function callCallback() { - if (callback) { - protectCall(callback, 'loadReceipts.callback', { - appStoreReceipt: that.appStoreReceipt, - forTransaction: function (transactionId) { - return that.receiptForTransaction[transactionId] || null; - }, - forProduct: function (productId) { - return that.receiptForProduct[productId] || null; - } - }); - } + protectCall(callback, 'loadReceipts.callback', { + appStoreReceipt: that.appStoreReceipt, + forTransaction: function (transactionId) { + return that.receiptForTransaction[transactionId] || null; + }, + forProduct: function (productId) { + return that.receiptForProduct[productId] || null; + } + }); + } + + if (that.appStoreReceipt) { + log('appStoreReceipt already loaded:'); + log(that.appStoreReceipt); + callCallback(); } + else { + log('loading appStoreReceipt'); + exec('appStoreReceipt', [], loaded, error); + } +}; - exec('appStoreReceipt', [], loaded, error); +InAppPurchase.prototype.setAppStoreReceipt = function(base64) { + this.appStoreReceipt = base64; + if (window.localStorage && base64) { + window.localStorage.sk_appStoreReceipt = base64; + } +}; +InAppPurchase.prototype.loadAppStoreReceipt = function() { + if (window.localStorage && window.localStorage.sk_appStoreReceipt) { + this.appStoreReceipt = window.localStorage.sk_appStoreReceipt; + } + if (this.appStoreReceipt === 'null') + this.appStoreReceipt = null; }; /* diff --git a/src/js/product.js b/src/js/product.js index 1a27db90..bf87a185 100644 --- a/src/js/product.js +++ b/src/js/product.js @@ -165,12 +165,20 @@ store.Product.prototype.verify = function() { }); } if (data.code === store.PURCHASE_EXPIRED) { - store.error(err); - store.utils.callExternal('verify.error', errorCb, err); - store.utils.callExternal('verify.done', doneCb, that); - that.trigger("expired"); - that.set("state", store.VALID); - store.utils.callExternal('verify.expired', expiredCb, that); + if (nRetry < 2 && store._refreshForValidation) { + nRetry += 1; + store._refreshForValidation(function() { + delay(that, tryValidation, 300); + }); + } + else { + store.error(err); + store.utils.callExternal('verify.error', errorCb, err); + store.utils.callExternal('verify.done', doneCb, that); + that.trigger("expired"); + that.set("state", store.VALID); + store.utils.callExternal('verify.expired', expiredCb, that); + } } else if (nRetry < 4) { // It failed... let's try one more time. Maybe the appStoreReceipt wasn't updated yet. diff --git a/test/js/test-ios.js b/test/js/test-ios.js index 11291fbe..9e81924a 100644 --- a/test/js/test-ios.js +++ b/test/js/test-ios.js @@ -4,9 +4,11 @@ var assert = require("assert"); var store = require("../tmp/store-test"); var helper = require("./helper"); +(function() { +"use strict"; global.store = store; global.document = { - addEventListener: function(/*event, callback*/) { "use strict"; } + addEventListener: function(/*event, callback*/) {} }; global.localStorage = {}; @@ -14,7 +16,6 @@ global.localStorage = {}; global.storekit = { initShouldFail: false, init: function(options, success, error) { - "use strict"; this.options = options; this.initCalled = (this.initCalled || 0) + 1; if (this.initShouldFail) { @@ -28,7 +29,6 @@ global.storekit = { }, loadShouldFail: false, load: function(products, success, error) { - "use strict"; this.products = products; this.loadCalled = (this.loadCalled || 0) + 1; if (this.loadShouldFail) { @@ -43,8 +43,19 @@ global.storekit = { }), ["cc.fovea.i"]); } + }, + refreshReceipts: function(s/*,e*/) { + if (s) { + s(null); + } + }, + loadReceipts: function(cb) { + if (cb) { + cb({}); + } } }; +})(); describe('iOS', function(){ "use strict"; diff --git a/www/store-android.js b/www/store-android.js index 12c3728c..3365541c 100644 --- a/www/store-android.js +++ b/www/store-android.js @@ -112,12 +112,19 @@ store.verbosity = 0; }); } if (data.code === store.PURCHASE_EXPIRED) { - store.error(err); - store.utils.callExternal("verify.error", errorCb, err); - store.utils.callExternal("verify.done", doneCb, that); - that.trigger("expired"); - that.set("state", store.VALID); - store.utils.callExternal("verify.expired", expiredCb, that); + if (nRetry < 2 && store._refreshForValidation) { + nRetry += 1; + store._refreshForValidation(function() { + delay(that, tryValidation, 300); + }); + } else { + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); + that.trigger("expired"); + that.set("state", store.VALID); + store.utils.callExternal("verify.expired", expiredCb, that); + } } else if (nRetry < 4) { nRetry += 1; delay(this, tryValidation, 1e3 * nRetry * nRetry); diff --git a/www/store-ios.js b/www/store-ios.js index 8e084ed6..30593e04 100644 --- a/www/store-ios.js +++ b/www/store-ios.js @@ -112,12 +112,19 @@ store.verbosity = 0; }); } if (data.code === store.PURCHASE_EXPIRED) { - store.error(err); - store.utils.callExternal("verify.error", errorCb, err); - store.utils.callExternal("verify.done", doneCb, that); - that.trigger("expired"); - that.set("state", store.VALID); - store.utils.callExternal("verify.expired", expiredCb, that); + if (nRetry < 2 && store._refreshForValidation) { + nRetry += 1; + store._refreshForValidation(function() { + delay(that, tryValidation, 300); + }); + } else { + store.error(err); + store.utils.callExternal("verify.error", errorCb, err); + store.utils.callExternal("verify.done", doneCb, that); + that.trigger("expired"); + that.set("state", store.VALID); + store.utils.callExternal("verify.expired", expiredCb, that); + } } else if (nRetry < 4) { nRetry += 1; delay(this, tryValidation, 1e3 * nRetry * nRetry); @@ -784,6 +791,7 @@ store.verbosity = 0; protectCall(options.error, "options.error", InAppPurchase.prototype.ERR_SETUP, "Setup failed"); protectCall(error, "init.error"); }; + this.loadAppStoreReceipt(); exec("setup", [], setupOk, setupFailed); }; InAppPurchase.prototype.purchase = function(productId, quantity) { @@ -907,24 +915,37 @@ store.verbosity = 0; if (this.needRestoreNotification) delete this.needRestoreNotification; else return; protectCall(this.options.restoreFailed, "options.restoreFailed", errorCode); }; - InAppPurchase.prototype.refreshReceipts = function() { + InAppPurchase.prototype.refreshReceipts = function(successCb, errorCb) { var that = this; - that.appStoreReceipt = null; - var loaded = function(base64) { - that.appStoreReceipt = base64; - protectCall(that.options.receiptsRefreshed, "options.receiptsRefreshed", base64); + var loaded = function(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); + that.setAppStoreReceipt(base64); + protectCall(that.options.receiptsRefreshed, "options.receiptsRefreshed", { + appStoreReceipt: base64, + bundleIdentifier: bundleIdentifier, + bundleShortVersion: bundleShortVersion, + bundleNumericVersion: bundleNumericVersion, + bundleSignature: bundleSignature + }); + protectCall(successCb, "refreshReceipts.success", base64); }; var error = function(errMessage) { log("refresh receipt failed: " + errMessage); protectCall(that.options.error, "options.error", InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, "Failed to refresh receipt: " + errMessage); + protectCall(errorCb, "refreshReceipts.error", InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, "Failed to refresh receipt: " + errMessage); }; + log("refreshing appStoreReceipt"); exec("appStoreRefreshReceipt", [], loaded, error); }; InAppPurchase.prototype.loadReceipts = function(callback) { var that = this; - that.appStoreReceipt = null; var loaded = function(base64) { - that.appStoreReceipt = base64; + that.setAppStoreReceipt(base64); callCallback(); }; var error = function(errMessage) { @@ -932,19 +953,36 @@ store.verbosity = 0; protectCall(that.options.error, "options.error", InAppPurchase.prototype.ERR_LOAD_RECEIPTS, "Failed to load receipt: " + errMessage); }; function callCallback() { - if (callback) { - protectCall(callback, "loadReceipts.callback", { - appStoreReceipt: that.appStoreReceipt, - forTransaction: function(transactionId) { - return that.receiptForTransaction[transactionId] || null; - }, - forProduct: function(productId) { - return that.receiptForProduct[productId] || null; - } - }); - } + protectCall(callback, "loadReceipts.callback", { + appStoreReceipt: that.appStoreReceipt, + forTransaction: function(transactionId) { + return that.receiptForTransaction[transactionId] || null; + }, + forProduct: function(productId) { + return that.receiptForProduct[productId] || null; + } + }); + } + if (that.appStoreReceipt) { + log("appStoreReceipt already loaded:"); + log(that.appStoreReceipt); + callCallback(); + } else { + log("loading appStoreReceipt"); + exec("appStoreReceipt", [], loaded, error); + } + }; + InAppPurchase.prototype.setAppStoreReceipt = function(base64) { + this.appStoreReceipt = base64; + if (window.localStorage && base64) { + window.localStorage.sk_appStoreReceipt = base64; + } + }; + InAppPurchase.prototype.loadAppStoreReceipt = function() { + if (window.localStorage && window.localStorage.sk_appStoreReceipt) { + this.appStoreReceipt = window.localStorage.sk_appStoreReceipt; } - exec("appStoreReceipt", [], loaded, error); + if (this.appStoreReceipt === "null") this.appStoreReceipt = null; }; InAppPurchase.prototype.runQueue = function() { if (!this.eventQueue.length || !this.onPurchased && !this.onFailed && !this.onRestored) { @@ -1102,8 +1140,8 @@ store.verbosity = 0; p.trigger("loaded"); } setTimeout(function() { - storekit.loading = false; - storekit.loaded = true; + loading = false; + loaded = true; store.ready(true); }, 1); } @@ -1112,6 +1150,28 @@ store.verbosity = 0; loading = false; retry(storekitLoad); } + var refreshCallbacks = []; + var refreshing = false; + function storekitRefreshReceipts(callback) { + if (callback) refreshCallbacks.push(callback); + if (refreshing) return; + refreshing = true; + function callCallbacks() { + var callbacks = refreshCallbacks; + refreshCallbacks = []; + for (var i = 0; i < callbacks.length; ++i) callbacks[i](); + } + storekit.refreshReceipts(function() { + refreshing = false; + callCallbacks(); + }, function() { + refreshing = false; + callCallbacks(); + }); + } + store.when("expired", function() { + storekitRefreshReceipts(); + }); function storekitPurchasing(productId) { store.log.debug("ios -> is purchasing " + productId); store.ready(function() { @@ -1179,6 +1239,27 @@ store.verbosity = 0; } store.when("re-refreshed", function() { storekit.restore(); + storekit.refreshReceipts(function(data) { + if (data) { + var p = data.bundleIdentifier ? store.get(data.bundleIdentifier) : null; + if (!p) { + p = new store.Product({ + id: data.bundleIdentifier || "application data", + alias: "application data", + type: store.NON_CONSUMABLE + }); + store.register(p); + } + p.version = data.bundleShortVersion; + p.transaction = { + type: "ios-appstore", + appStoreReceipt: data.appStoreReceipt, + signature: data.signature + }; + p.trigger("loaded"); + p.set("state", store.APPROVED); + } + }); }); function storekitRestored(originalTransactionId, productId) { store.log.info("ios -> restored purchase " + productId); @@ -1194,17 +1275,34 @@ store.verbosity = 0; message: "Failed to restore purchases during refresh" }); } + store._refreshForValidation = function(callback) { + storekitRefreshReceipts(callback); + }; store._prepareForValidation = function(product, callback) { - storekit.loadReceipts(function(r) { - if (!product.transaction) { - product.transaction = { - type: "ios-appstore" - }; - } - product.transaction.appStoreReceipt = r.appStoreReceipt; - if (product.transaction.id) product.transaction.transactionReceipt = r.forTransaction(product.transaction.id); - callback(); - }); + var nRetry = 0; + function loadReceipts() { + storekit.loadReceipts(function(r) { + if (!product.transaction) { + product.transaction = { + type: "ios-appstore" + }; + } + product.transaction.appStoreReceipt = r.appStoreReceipt; + if (product.transaction.id) product.transaction.transactionReceipt = r.forTransaction(product.transaction.id); + if (!product.transaction.appStoreReceipt && !product.transaction.transactionReceipt) { + nRetry++; + if (nRetry < 2) { + setTimeout(loadReceipts, 500); + return; + } else if (nRetry === 2) { + storekit.refreshReceipts(loadReceipts); + return; + } + } + callback(); + }); + } + loadReceipts(); }; function isOwned(productId) { return localStorage["__cc_fovea_store_ios_owned_ " + productId] === "1"; From e0ef39cb3d0d44fbcf6dc1df2197d048d8a3df8f Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Tue, 18 Nov 2014 08:30:30 +0200 Subject: [PATCH 09/11] Ignore unregistered products on Android. Closes #78 --- src/js/platforms/android-adapter.js | 27 +++++++++++++++++---------- www/store-android.js | 22 ++++++++++++---------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/js/platforms/android-adapter.js b/src/js/platforms/android-adapter.js index 5fea6f25..5f45a68b 100644 --- a/src/js/platforms/android-adapter.js +++ b/src/js/platforms/android-adapter.js @@ -46,19 +46,26 @@ function iabLoaded(validProducts) { store.log.debug("android -> loaded - " + JSON.stringify(validProducts)); var p, i; for (i = 0; i < validProducts.length; ++i) { - p = store.products.byId[validProducts[i].productId]; - p.set({ - title: validProducts[i].title, - price: validProducts[i].price, - description: validProducts[i].description, - currency: validProducts[i].price_currency_code, - state: store.VALID - }); - p.trigger("loaded"); + + if (validProducts[i].productId) + p = store.products.byId[validProducts[i].productId]; + else + p = null; + + if (p) { + p.set({ + title: validProducts[i].title, + price: validProducts[i].price, + description: validProducts[i].description, + currency: validProducts[i].price_currency_code, + state: store.VALID + }); + p.trigger("loaded"); + } } for (i = 0; i < skus.length; ++i) { p = store.products.byId[skus[i]]; - if (!p.valid) { + if (p && !p.valid) { p.set("state", store.INVALID); p.trigger("loaded"); } diff --git a/www/store-android.js b/www/store-android.js index 3365541c..6836077d 100644 --- a/www/store-android.js +++ b/www/store-android.js @@ -868,19 +868,21 @@ store.verbosity = 0; store.log.debug("android -> loaded - " + JSON.stringify(validProducts)); var p, i; for (i = 0; i < validProducts.length; ++i) { - p = store.products.byId[validProducts[i].productId]; - p.set({ - title: validProducts[i].title, - price: validProducts[i].price, - description: validProducts[i].description, - currency: validProducts[i].price_currency_code, - state: store.VALID - }); - p.trigger("loaded"); + if (validProducts[i].productId) p = store.products.byId[validProducts[i].productId]; else p = null; + if (p) { + p.set({ + title: validProducts[i].title, + price: validProducts[i].price, + description: validProducts[i].description, + currency: validProducts[i].price_currency_code, + state: store.VALID + }); + p.trigger("loaded"); + } } for (i = 0; i < skus.length; ++i) { p = store.products.byId[skus[i]]; - if (!p.valid) { + if (p && !p.valid) { p.set("state", store.INVALID); p.trigger("loaded"); } From cff2b79cc019b9dd6a9f8340af6d549adc7ed6c1 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Tue, 18 Nov 2014 13:53:27 +0200 Subject: [PATCH 10/11] Prepare for release 3.9.0-beta.4 --- package.json | 4 ++-- plugin.xml | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index fe74f359..6a727a49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cordova-purchase-plugin", - "version": "3.5.0-dev", - "description": "Cordova Purchase plugin for Apple AppStore for iOS and Google Play Store for Android", + "version": "3.9.0-beta.4", + "description": "Cordova Purchase plugin for iOS and Android (AppStore and PlayStore)", "repository": { "type": "git", "url": "https://github.com/j3k0/PhoneGap-InAppPurchase-iOS.git" diff --git a/plugin.xml b/plugin.xml index 64e2c777..e6ee0183 100644 --- a/plugin.xml +++ b/plugin.xml @@ -3,16 +3,16 @@ xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cc.fovea.cordova.purchase" - version="3.9.0-beta.1"> + version="3.9.0-beta.4"> Purchase - Cordova Purchase plugin for Apple AppStore for iOS and Google Play Store for Android + Cordova Purchase plugin for iOS and Android (AppStore and PlayStore) - https://github.com/j3k0/PhoneGap-InAppPurchase-iOS.git - https://github.com/j3k0/PhoneGap-InAppPurchase-iOS/issues - + https://github.com/j3k0/cordova-plugin-purchase.git + https://github.com/j3k0/cordova-plugin-purchase/issues + MIT cordova,phonegap,purchase,storekit,ios,android,play,appstore @@ -21,10 +21,10 @@ - + - + @@ -53,11 +53,11 @@ - + - + From fc723fe81a3d0fe7f7f3cdadb17957b13d12fb0e Mon Sep 17 00:00:00 2001 From: Jean-Christophe Hoelt Date: Sat, 29 Nov 2014 16:18:40 +0200 Subject: [PATCH 11/11] Prepare release 3.9.1 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6a727a49..292dfedd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-purchase-plugin", - "version": "3.9.0-beta.4", + "version": "3.9.1", "description": "Cordova Purchase plugin for iOS and Android (AppStore and PlayStore)", "repository": { "type": "git", diff --git a/plugin.xml b/plugin.xml index e6ee0183..b89f7191 100644 --- a/plugin.xml +++ b/plugin.xml @@ -3,7 +3,7 @@ xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cc.fovea.cordova.purchase" - version="3.9.0-beta.4"> + version="3.9.1"> Purchase Cordova Purchase plugin for iOS and Android (AppStore and PlayStore)