2

我创建了一个概念验证的 PhoneGap 应用程序来测试 iOS 上的应用内购买机制。该应用程序基于 Phonegap 2.9.0,使用这个 InAppPurchase 插件,并且大致基于解释如何使用该插件的本教程。

问题是在成功从 Apple 服务器接收到 InApp Purchase 数据后,Objective-C 插件没有执行 Javascript 回调函数。我不知道为什么 JS 没有被执行,所以希望有人能发现问题......?

当我使用 XCode 4.6.3 在 iPhone 4S 上运行我的应用程序时,一切正常,直到 StoreKit API在接收到 InApp Purchase 项目的产品数据后异步调用productsRequest成功回调。InAppPurchase.m我可以NSLog在第 213 行看到语句的输出,该输出callbackArgs在 XCode 日志窗口中输出,其中包含 InApp 购买项目的正确详细信息。之后的行将导致执行 Javascript 成功回调,该回调在第 128 行定义InAppPurchase.js并在第 140 行传入,但第 129 行的日志输出永远不会出现在 XCode 日志窗口中。

如果我在 XCode 中使用断点单步执行 Objective-C,我可以看到该callbackId变量具有合理的值,并且我可以单步self.plugin.commandDelegate执行 Cordova 代码到构造 JS 回调的位置,这一切看起来都很好,但 JS 从来没有真正运行。

我也尝试在应用程序中使用 Phonegap 2.7.0,但结果是一样的。

我的应用程序的 XCode 项目可以从这里下载

2013 年 8 月 19 日更新:关于如何使用此插件的教程 的作者已确认该插件的此问题是可重现的,但尚未找到原因/解决方案。我还没有看到这个插件成功运行的例子。

源代码和输出

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();
});
4

1 回答 1

0

我将问题归结为两件事:首先,控制台输出没有立即出现,因为 Objective-C 成功回调函数“didReceiveResponse”在不同的线程上返回——按下电源按钮暂停应用程序会刷新缓冲的日志内容到控制台。

其次,我的成功处理函数(对未定义变量的引用)中的 JS 错误正在静默失败,所以这并不明显。

于 2013-10-17T18:13:01.327 回答