ios - InAppPurchase plugin failing to execute JS callback from Objective-C -


i’ve created proof-of-concept phonegap app test out in-app purchase mechanism on ios. app based on phonegap 2.9.0, uses this inapppurchase plugin , loosely based on this tutorial explains how use plugin.

the problem javascript callback function not being executed objective-c plugin upon receiving inapp purchase data apple server. i’ve no idea why js not getting executed hoping can spot problem...?

when run app on iphone 4s using xcode 4.6.3, works until storekit api asynchronously calls productsrequest success callback in inapppurchase.m upon receiving product data inapp purchase items. can see output of nslog statement on line 213 outputs callbackargs in xcode log window, contains correct details of inapp purchase items. line after should result in javascript success callback being executed, defined on line 128 of inapppurchase.js , passed in on line 140, log output @ line 129 never appears in xcode log window.

if step through objective-c using breakpoint in xcode, can see callbackid variable has sensible value , can step through self.plugin.commanddelegate cordova code js callback constructed , seems fine js never runs.

i tried using phonegap 2.7.0 app, result same.

my xcode project app can downloaded from here

update 19/08/2013: author of a tutorial on how use plugin has confirmed problem plugin reproducible has yet find cause/solution. i've yet see example of plugin working successfully.

source code , output

log output xcode (excuse fraggles , wombles, i’m child of 80’s):

2013-08-07 16:16:48.137 inapptest[347:907] multi-tasking -> device: yes, app: yes 2013-08-07 16:16:48.959 inapptest[347:907] resetting plugins due page load. 2013-08-07 16:16:49.342 inapptest[347:907] finished load of: file:///var/mobile/applications/62132e03-9de3-4b01-8066-1978cabdd91f/inapptest.app/www/index.html 2013-08-07 16:16:49.479 inapptest[347:907] deprecation notice: connection reachableviawwan return value of '2g' deprecated of cordova version 2.6.0 , changed 'cellular' in future release.  2013-08-07 16:16:49.514 inapptest[347:907] trace: environment ready 2013-08-07 16:16:49.516 inapptest[347:907] device ready 2013-08-07 16:16:49.517 inapptest[347:907] initialising iap... 2013-08-07 16:16:49.519 inapptest[347:907] inapppurchase[js]: setup ok 2013-08-07 16:16:49.520 inapptest[347:907] iap ready 2013-08-07 16:16:49.521 inapptest[347:907] inapppurchase[js]: load ["uk.co.workingedge.test.inapp.fraggleguide","uk.co.workingedge.test.inapp.wombleguide"] 2013-08-07 16:16:49.522 inapptest[347:907] inapppurchase[objc]: getting products data 2013-08-07 16:16:49.524 inapptest[347:907] inapppurchase[objc]: set has 2 elements 2013-08-07 16:16:49.525 inapptest[347:907] inapppurchase[objc]: - uk.co.workingedge.test.inapp.fraggleguide 2013-08-07 16:16:49.526 inapptest[347:907] inapppurchase[objc]: - uk.co.workingedge.test.inapp.wombleguide 2013-08-07 16:16:49.527 inapptest[347:907] inapppurchase[objc]: start 2013-08-07 16:16:51.056 inapptest[347:907] inapppurchase[objc]: productsrequest: didreceiveresponse: 2013-08-07 16:16:51.058 inapptest[347:907] inapppurchase[objc]: has 2 validproducts 2013-08-07 16:16:51.058 inapptest[347:907] inapppurchase[objc]: - uk.co.workingedge.test.inapp.fraggleguide: fraggle guide 2013-08-07 16:16:51.062 inapptest[347:907] inapppurchase[objc]: - uk.co.workingedge.test.inapp.wombleguide: womble guide 2013-08-07 16:16:51.065 inapptest[347:907] inapppurchase[objc]: productsrequest: didreceiveresponse: sendpluginresult: (         (                 {             description = "guide fraggles";             id = "uk.co.workingedge.test.inapp.fraggleguide";             price = "\u00a30.69";             title = "fraggle guide";         },                 {             description = "guide wombles";             id = "uk.co.workingedge.test.inapp.wombleguide";             price = "\u00a30.69";             title = "womble guide";         }     ),         (     ) ) [end of log] 

inapppurchase.m

// //  inapppurchase.m // //  created matt kane on 20/02/2011. //  copyright (c) matt kane 2011. rights reserved. //  copyright (c) jean-christophe hoelt 2013 //  #import "inapppurchase.h"  // create nsnull objects nil items (since neither nsarray nor nsdictionary can store nil values). #define nilable(obj) ((obj) != nil ? (nsobject *)(obj) : (nsobject *)[nsnull null])  // avoid compilation warning, declare jsonkit , sbjson's // category methods without including header files. @interface nsarray (stubsforserializers) - (nsstring *)jsonstring; - (nsstring *)jsonrepresentation; @end  // helper category method choose json serializer use. @interface nsarray (jsonserialize) - (nsstring *)jsonserialize; @end  @implementation nsarray (jsonserialize) - (nsstring *)jsonserialize {     return [self respondstoselector:@selector(jsonstring)] ? [self jsonstring] : [self jsonrepresentation]; } @end  @implementation inapppurchase @synthesize list;  -(void) setup: (cdvinvokedurlcommand*)command {     cdvpluginresult* pluginresult = nil;     self.list = [[nsmutabledictionary alloc] init];     [[skpaymentqueue defaultqueue] addtransactionobserver:self];     pluginresult = [cdvpluginresult resultwithstatus:cdvcommandstatus_ok messageasstring:@"inapppurchase initialized"];     [self.commanddelegate sendpluginresult:pluginresult callbackid:command.callbackid]; }  /**  * request product data given productids.  * see js further documentation.  */ - (void) load: (cdvinvokedurlcommand*)command {     nslog(@"inapppurchase[objc]: getting products data");      nsarray *inarray = [command.arguments objectatindex:0];      if ((unsigned long)[inarray count] == 0) {         nslog(@"inapppurchase[objc]: empty array");         nsarray *callbackargs = [nsarray arraywithobjects: nil, nil, nil];         cdvpluginresult* pluginresult = [cdvpluginresult resultwithstatus:cdvcommandstatus_ok messageasarray:callbackargs];         [self.commanddelegate sendpluginresult:pluginresult callbackid:command.callbackid];         return;     }      if (![[inarray objectatindex:0] iskindofclass:[nsstring class]]) {         nslog(@"inapppurchase[objc]: not array of nsstring");         cdvpluginresult* pluginresult = [cdvpluginresult resultwithstatus:cdvcommandstatus_error messageasstring:@"invalid arguments"];         [self.commanddelegate sendpluginresult:pluginresult callbackid:command.callbackid];         return;     }      nsset *productidentifiers = [nsset setwitharray:inarray];     nslog(@"inapppurchase[objc]: set has %li elements", (unsigned long)[productidentifiers count]);     (nsstring *item in productidentifiers) {         nslog(@"inapppurchase[objc]: - %@", item);     }     skproductsrequest *productsrequest = [[skproductsrequest alloc] initwithproductidentifiers:productidentifiers];      batchproductsrequestdelegate* delegate = [[[batchproductsrequestdelegate alloc] init] retain];     delegate.plugin = self;     delegate.command = command;      productsrequest.delegate = delegate;     nslog(@"inapppurchase[objc]: start");     [productsrequest start]; }  - (void) purchase: (cdvinvokedurlcommand*)command {     nslog(@"inapppurchase[objc]: iap");     id identifier = [command.arguments objectatindex:0];     id quantity =   [command.arguments objectatindex:1];      skmutablepayment *payment = [skmutablepayment paymentwithproduct:[self.list objectforkey:identifier]];     if ([quantity respondstoselector:@selector(integervalue)]) {         payment.quantity = [quantity integervalue];     }     [[skpaymentqueue defaultqueue] addpayment:payment]; }  - (void) restorecompletedtransactions: (cdvinvokedurlcommand*)command {     [[skpaymentqueue defaultqueue] restorecompletedtransactions]; }  // skpaymenttransactionobserver methods // called when transaction status updated // - (void)paymentqueue:(skpaymentqueue*)queue updatedtransactions:(nsarray*)transactions {     nsstring *state, *error, *transactionidentifier, *transactionreceipt, *productid;     nsinteger errorcode;      (skpaymenttransaction *transaction in transactions)     {         error = state = transactionidentifier = transactionreceipt = productid = @"";         errorcode = 0;          switch (transaction.transactionstate)         {             case skpaymenttransactionstatepurchasing:                 nslog(@"inapppurchase[objc]: purchasing...");                 continue;              case skpaymenttransactionstatepurchased:                 state = @"paymenttransactionstatepurchased";                 transactionidentifier = transaction.transactionidentifier;                 transactionreceipt = [[transaction transactionreceipt] base64encodedstring];                 productid = transaction.payment.productidentifier;                 break;              case skpaymenttransactionstatefailed:                 state = @"paymenttransactionstatefailed";                 error = transaction.error.localizeddescription;                 errorcode = transaction.error.code;                 nslog(@"inapppurchase[objc]: error %d %@", errorcode, error);                 break;              case skpaymenttransactionstaterestored:                 state = @"paymenttransactionstaterestored";                 transactionidentifier = transaction.originaltransaction.transactionidentifier;                 transactionreceipt = [[transaction transactionreceipt] base64encodedstring];                 productid = transaction.originaltransaction.payment.productidentifier;                 break;              default:                 nslog(@"inapppurchase[objc]: invalid state");                 continue;         }         nslog(@"inapppurchase[objc]: state: %@", state);         nsarray *callbackargs = [nsarray arraywithobjects:                                  nilable(state),                                  [nsnumber numberwithint:errorcode],                                  nilable(error),                                  nilable(transactionidentifier),                                  nilable(productid),                                  nilable(transactionreceipt),                                  nil];         cdvpluginresult* pluginresult = nil;         pluginresult = [cdvpluginresult resultwithstatus:cdvcommandstatus_ok messageasarray: callbackargs];         nsstring *js = [nsstring             stringwithformat:@"window.storekit.updatedtransactioncallback.apply(window.storekit, %@)",             [callbackargs jsonserialize]];         nslog(@"inapppurchase[objc]: js: %@", js);         [self.commanddelegate evaljs:js];         [[skpaymentqueue defaultqueue] finishtransaction:transaction];     } }  - (void)paymentqueue:(skpaymentqueue *)queue restorecompletedtransactionsfailedwitherror:(nserror *)error {     /* nsstring *js = [nsstring stringwithformat:       @"window.storekit.onrestorecompletedtransactionsfailed(%d)", error.code];     [self writejavascript: js]; */ }  - (void)paymentqueuerestorecompletedtransactionsfinished:(skpaymentqueue *)queue {     /* nsstring *js = @"window.storekit.onrestorecompletedtransactionsfinished()";     [self writejavascript: js]; */ }  @end  /**  * receives product data multiple productids , passes arrays of  * js objects containing these data single callback method.  */ @implementation batchproductsrequestdelegate  @synthesize plugin, command;  - (void)productsrequest:(skproductsrequest*)request didreceiveresponse:(skproductsresponse*)response {      nslog(@"inapppurchase[objc]: productsrequest: didreceiveresponse:");     nsmutablearray *validproducts = [nsmutablearray array];     nslog(@"inapppurchase[objc]: has %li validproducts", (unsigned long)[response.products count]);     (skproduct *product in response.products) {         nslog(@"inapppurchase[objc]: - %@: %@", product.productidentifier, product.localizedtitle);         [validproducts addobject:          [nsdictionary dictionarywithobjectsandkeys:           nilable(product.productidentifier),    @"id",           nilable(product.localizedtitle),       @"title",           nilable(product.localizeddescription), @"description",           nilable(product.localizedprice),       @"price",           nil]];         [self.plugin.list setobject:product forkey:[nsstring stringwithformat:@"%@", product.productidentifier]];     }      nsarray *callbackargs = [nsarray arraywithobjects:                              nilable(validproducts),                              nilable(response.invalidproductidentifiers),                              nil];      cdvpluginresult* pluginresult =       [cdvpluginresult resultwithstatus:cdvcommandstatus_ok messageasarray:callbackargs];     nslog(@"inapppurchase[objc]: productsrequest: didreceiveresponse: sendpluginresult: %@", callbackargs);     [self.plugin.commanddelegate sendpluginresult:pluginresult callbackid:self.command.callbackid];      [request release];     [self    release]; }  - (void) dealloc {     [plugin  release];     [command release];     [super   dealloc]; }  @end 

inapppurchase.js

/**   * plugin enable ios in-app purchases.  *  * copyright (c) matt kane 2011  * copyright (c) guillaume charhon 2012  * copyright (c) jean-christophe hoelt 2013  */  cordova.define("cordova/plugin/inapppurchase", function(require, exports, module) {     var exec = function (methodname, options, success, error) {         cordova.exec(success, error, "inapppurchase", methodname, options);     };      var log = function (msg) {         console.log("inapppurchase[js]: " + msg);     };      var inapppurchase = function() {         this.options = {};     };      // error codes.     inapppurchase.err_setup = 1;     inapppurchase.err_load = 2;     inapppurchase.err_purchase = 3;      inapppurchase.prototype.init = function (options) {         this.options = {             ready:    options.ready || function () {},             purchase: options.purchase || function () {},             restore:  options.restore || function () {},             restorefailed:  options.restorefailed || function () {},             restorecompleted:  options.restorecompleted || function () {},             error:    options.error || function () {}         };          var = this;         var setupok = function () {             log('setup ok');             that.options.ready();              // there reason why wouldn't automatically?             // yes! ask user password.             // that.restore();         };         var setupfailed = function () {             log('setup failed');             options.error(inapppurchase.err_setup, 'setup failed');         };          exec('setup', [], setupok, setupfailed);     };      /**      * makes in-app purchase.       *       * @param {string} productid product identifier. e.g. "com.example.myapp.myproduct"      * @param {int} quantity       */     inapppurchase.prototype.purchase = function (productid, quantity) {         quantity = (quantity|0) || 1;         var options = this.options;         var purchaseok = function () {             log('purchased ' + productid);             if (typeof options.purchase === 'function')                 options.purchase(productid, quantity);         };         var purchasefailed = function () {             var msg = 'purchasing ' + productid + ' failed';             log(msg);             if (typeof options.error === 'function')                 options.error(inapppurchase.err_purchase, msg, productid, quantity);         };         return exec('purchase', [productid, quantity], purchaseok, purchasefailed);     };      /**      * asks payment queue restore completed purchases.      * restored transactions passed onrestored callback, make sure define handler first.      *       */     inapppurchase.prototype.restore = function() {         return exec('restorecompletedtransactions', []);     };      /**      * retrieves localized product data, including price (as localized      * string), name, description of multiple products.      *      * @param {array} productids      *   array of product identifier strings.      *      * @param {function} callback      *   called once result of products request. signature:      *      *     function(validproducts, invalidproductids)      *      *   validproducts receives array of objects of form:      *      *     {      *       id: "<productid>",      *       title: "<localised title>",      *       description: "<localised escription>",      *       price: "<localised price>"      *     }      *      *  , invalidproductids receives array of product identifier      *  strings rejected app store.      */     inapppurchase.prototype.load = function (productids, callback) {         var options = this.options;         if (typeof productids === "string") {             productids = [productids];         }         if (!productids.length) {             // empty array, nothing do.             callback([], []);         }         else {             if (typeof productids[0] !== 'string') {                 var msg = 'invalid productids given store.load: ' + json.stringify(productids);                 log(msg);                 options.error(inapppurchase.err_load, msg);                 return;             }             log('load ' + json.stringify(productids));              var loadok = function (array) {                 log("loadok()");                 var valid = array[0];                 var invalid = array[1];                 log('load ok: { valid:' + json.stringify(valid) + ' invalid:' + json.stringify(invalid) + ' }');                 callback(valid, invalid);             };             var loadfailed = function (errmessage) {                 log('load failed: ' + errmessage);                 options.error(inapppurchase.err_load, 'failed load product data: ' + errmessage);             };              exec('load', [productids], loadok, loadfailed);         }     };      /* called native.*/     inapppurchase.prototype.updatedtransactioncallback = function (state, errorcode, errortext, transactionidentifier, productid, transactionreceipt) {         // alert(state);         switch(state) {             case "paymenttransactionstatepurchased":                 this.options.purchase(transactionidentifier, productid, transactionreceipt);                 return;              case "paymenttransactionstatefailed":                 this.options.error(errorcode, errortext);                 return;             case "paymenttransactionstaterestored":                 this.options.restore(transactionidentifier, productid, transactionreceipt);                 return;         }     };      inapppurchase.prototype.restorecompletedtransactionsfinished = function () {         this.options.restorecompleted();     };      inapppurchase.prototype.restorecompletedtransactionsfailed = function (errorcode) {         this.options.restorefailed(errorcode);     };      /*      * queue stuff here because may sent events before listeners have been registered. because if have       * incomplete transactions when quit, app try run these when resume. if don't register receive these      * right away may missed. callback has been registered sent events waiting      * in queue.      */     inapppurchase.prototype.runqueue = function () {         if(!this.eventqueue.length || (!this.onpurchased && !this.onfailed && !this.onrestored)) {             return;         }         var args;         /* can't work directly on queue, because we're pushing new elements onto */         var queue = this.eventqueue.slice();         this.eventqueue = [];         args = queue.shift();         while (args) {             this.updatedtransactioncallback.apply(this, args);             args = queue.shift();         }         if (!this.eventqueue.length) {               this.unwatchqueue();         }     };      inapppurchase.prototype.watchqueue = function () {         if (this.timer) {             return;         }         this.timer = window.setinterval(function () {             window.storekit.runqueue();         }, 10000);     };      inapppurchase.prototype.unwatchqueue = function () {         if (this.timer) {             window.clearinterval(this.timer);             this.timer = null;         }     };      inapppurchase.eventqueue = [];     inapppurchase.timer = null;      module.exports = new inapppurchase(); }); 

i tracked issue down 2 things: first console output not appearing right away because objective-c success callback function “didreceiveresponse” comes on different thread – pressing power button pause app flushes buffered log content console.

secondly, js error in success handler function (reference undefined variable) failing silently not obvious.


Comments

Popular posts from this blog

matlab - Deleting rows with specific rules -

php - MySQLi multi_query results for later use -