InAppPurchase 插件无法从 Objective-C 执行 JS 回调
Posted
技术标签:
【中文标题】InAppPurchase 插件无法从 Objective-C 执行 JS 回调【英文标题】:InAppPurchase plugin failing to execute JS callback from Objective-C 【发布时间】:2013-08-08 10:15:02 【问题描述】:我创建了一个概念验证的 PhoneGap 应用来测试 ios 上的应用内购买机制。该应用基于Phonegap 2.9.0,使用this InAppPurchase plugin,大致基于this tutorial,它解释了如何使用插件。
问题是Objective-C插件在从Apple服务器成功接收到InApp Purchase数据后没有执行javascript回调函数。我不知道为什么 JS 没有被执行,所以希望有人能发现问题......?
当我使用 XCode 4.6.3 在 iPhone 4S 上运行我的应用程序时,一切正常,直到 StoreKit API 在接收到 InApp 购买项目的产品数据时异步调用 InAppPurchase.m
中的 productsRequest
成功回调。我可以在第 213 行看到 NSLog
语句的输出,该语句在 XCode 日志窗口中输出 callbackArgs
,其中包含 InApp 购买项目的正确详细信息。之后的行应该会导致执行 Javascript 成功回调,该回调在 InAppPurchase.js
的第 128 行定义并在第 140 行传入,但第 129 行的日志输出永远不会出现在 XCode 日志窗口中。
如果我在 XCode 中使用断点单步执行 Objective-C,我可以看到 callbackId
变量有一个合理的值,我可以单步执行 self.plugin.commandDelegate
进入 Cordova 代码到构造 JS 回调的位置和这一切看起来都很好,但 JS 从未真正运行过。
我也尝试在应用程序中使用 Phonegap 2.7.0,但结果是一样的。
我的应用程序的 XCode 项目可以下载from here
2013 年 8 月 19 日更新: a tutorial on how to use this plugin 的作者有confirmed this problem with the plugin is reproducible,但还没有找到原因/解决方案。我还没有看到这个插件成功运行的例子。
源代码和输出
XCode 的日志输出(请原谅 Fraggles 和 Wombles,我是 80 后的孩子):
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 to 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: The Connection ReachableViaWWAN return value of '2g' is deprecated as of Cordova version 2.6.0 and will be changed to 'cellular' in a 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 to Fraggles";
id = "uk.co.workingedge.test.inapp.fraggleguide";
price = "\U00a30.69";
title = "Fraggle Guide";
,
description = "Guide to Wombles";
id = "uk.co.workingedge.test.inapp.wombleguide";
price = "\U00a30.69";
title = "Womble Guide";
),
(
)
)
[END OF LOG]
InAppPurchase.m
//
// InAppPurchase.m
//
// Created by Matt Kane on 20/02/2011.
// Copyright (c) Matt Kane 2011. All rights reserved.
// Copyright (c) Jean-Christophe Hoelt 2013
//
#import "InAppPurchase.h"
// Help create NSNull objects for nil items (since neither NSArray nor NSDictionary can store nil values).
#define NILABLE(obj) ((obj) != nil ? (NSObject *)(obj) : (NSObject *)[NSNull null])
// To avoid compilation warning, declare JSONKit and SBJson's
// category methods without including their header files.
@interface NSArray (StubsForSerializers)
- (NSString *)JSONString;
- (NSString *)JSONRepresentation;
@end
// Helper category method to choose which JSON serializer to 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 for the given productIds.
* See js for 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 an 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]);
for (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]: About to do 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 the transaction status is updated
//
- (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions
NSString *state, *error, *transactionIdentifier, *transactionReceipt, *productId;
NSInteger errorCode;
for (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 for multiple productIds and passes arrays of
* js objects containing these data to a 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]);
for (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
/**
* A plugin to 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 that = this;
var setupOk = function ()
log('setup ok');
that.options.ready();
// Is there a reason why we wouldn't like to do this automatically?
// YES! it does ask the user for his password.
// that.restore();
;
var setupFailed = function ()
log('setup failed');
options.error(InAppPurchase.ERR_SETUP, 'Setup failed');
;
exec('setup', [], setupOk, setupFailed);
;
/**
* Makes an in-app purchase.
*
* @param String productId The 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 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()
return exec('restoreCompletedTransactions', []);
;
/**
* Retrieves localized product data, including price (as localized
* string), name, description of multiple products.
*
* @param Array productIds
* An array of product identifier strings.
*
* @param Function callback
* Called once with the result of the products request. Signature:
*
* function(validProducts, invalidProductIds)
*
* where validProducts receives an array of objects of the form:
*
*
* id: "<productId>",
* title: "<localised title>",
* description: "<localised escription>",
* price: "<localised price>"
*
*
* and invalidProductIds receives an array of product identifier
* strings which were rejected by the app store.
*/
InAppPurchase.prototype.load = function (productIds, callback)
var options = this.options;
if (typeof productIds === "string")
productIds = [productIds];
if (!productIds.length)
// Empty array, nothing to do.
callback([], []);
else
if (typeof productIds[0] !== 'string')
var msg = 'invalid productIds given to 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 to load product data: ' + errMessage);
;
exec('load', [productIds], loadOk, loadFailed);
;
/* This is called from 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);
;
/*
* This queue stuff is here because we may be sent events before listeners have been registered. This is because if we have
* incomplete transactions when we quit, the app will try to run these when we resume. If we don't register to receive these
* right away then they may be missed. As soon as a callback has been registered then it will be sent any events waiting
* in the queue.
*/
InAppPurchase.prototype.runQueue = function ()
if(!this.eventQueue.length || (!this.onPurchased && !this.onFailed && !this.onRestored))
return;
var args;
/* We can't work directly on the queue, because we're pushing new elements onto it */
var queue = this.eventQueue.slice();
this.eventQueue = [];
args = queue.shift();
while (args)
this.updatedTransactionCallback.apply(this, args);
args = queue.shift();
if (!this.eventQueue.length)
this.unWatchQueue();
;
InAppPurchase.prototype.watchQueue = function ()
if (this.timer)
return;
this.timer = window.setInterval(function ()
window.storekit.runQueue();
, 10000);
;
InAppPurchase.prototype.unWatchQueue = function ()
if (this.timer)
window.clearInterval(this.timer);
this.timer = null;
;
InAppPurchase.eventQueue = [];
InAppPurchase.timer = null;
module.exports = new InAppPurchase();
);
【问题讨论】:
【参考方案1】:我将问题归结为两件事:首先,控制台输出没有立即出现,因为 Objective-C 成功回调函数“didReceiveResponse”在不同的线程上返回 - 按下电源按钮暂停应用程序会刷新将日志内容缓冲到控制台。
其次,我的成功处理函数(对未定义变量的引用)中的 JS 错误正在静默失败,所以这并不明显。
【讨论】:
您好 Dpa99c,对于追查问题的原因,这是个好消息。我刚刚按下电源按钮,确实立即出现了正确的日志。现在我的问题是:如何解决这个问题?我真的不能告诉我的用户按下电源按钮来激活应用内购买... 只有日志输出被缓冲(按下电源按钮刷新日志)。当 StoreKit API 收到来自远程 Apple 服务器的响应时,插件执行的回调不受此影响。因此,由于您的用户不需要查看日志,因此不会影响他们:-) 您好 Dpa99c,感谢您的评论。所以看起来当发生错误时,线程会卡住,直到使用设备电源按钮,如果没有发生错误,一切都会顺利进行?以上是关于InAppPurchase 插件无法从 Objective-C 执行 JS 回调的主要内容,如果未能解决你的问题,请参考以下文章
IOS InAppPurchase 内容从我自己的服务器下载