【发布时间】: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();
});
【问题讨论】:
标签: ios objective-c cordova in-app-purchase phonegap-plugins