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";