j3k0 / cordova-plugin-purchase

In-App Purchase for Cordova on iOS, Android and Windows
https://purchase.cordova.fovea.cc
1.3k stars 535 forks source link

Windows 10 consumePurchase could not convert to GUID #456

Closed trumpet2012 closed 8 years ago

trumpet2012 commented 8 years ago

Hi,

I am having trouble completing a purchase. When trying to buy a consumable product for the first time through the windows store I am receiving the following error:

Exception calling native with command :: InAppBillingPlugin :: consumePurchase ::exception=TypeError: Could not convert argument to type 'GUID'

It is able to trigger the windows buy modal and then goes through the process of validating the order with our server. It then reaches the finished state, before it throws the error.

Could this issue be simply because we are targeting windows 10 instead of 8.1?

I was digging around trying to find where this was being thrown and possibly why. I think the consumePurchase is being called in src/js/platforms/plugin-bridge.js here:

InAppBilling.prototype.consumePurchase = function (success, fail, productId) {
    if (this.options.showLog) {
        log('consumePurchase called!');
    }
    return cordova.exec(success, errorCb(fail), "InAppBillingPlugin", "consumePurchase", [productId]);
};

The only argument it passes is the product ID, while in the consumePurchase method in src/windows/InAppPurchaseProxy.js expects there to be two arguments passed. With the second argument expected to be the transaction ID.

consumePurchase: function (win, fail, args) {
        var fulfillmentResult = Windows.ApplicationModel.Store.FulfillmentResult;
        var productId = args[0];
        var transactionId = args[1];
        this.currentApp.reportConsumableFulfillmentAsync(productId, transactionId).done(

The transaction ID missing would fit with the error since windows expects this value to be a GUID (https://msdn.microsoft.com/en-us/windows/uwp/monetize/enable-consumable-in-app-product-purchases).

Here is the full log of the offending transaction:

Placing order:  [object Object] [object Object]
eval code (14) (4,29)
   "Placing order: "
   {
      [functions]: ,
      _queries: { },
      APPROVED: "approved",
      CONNECTION_FAILED: 6778002,
      CONSUMABLE: "consumable",
      DEBUG: 4,
      DOWNLOADED: "downloaded",
      DOWNLOADING: "downloading",
      ERR_BAD_RESPONSE: 6777018,
      ERR_CLIENT_INVALID: 6777005,
      ERR_COMMUNICATION: 6777014,
      ERR_DOWNLOAD: 6777021,
      ERR_FINISH: 6777013,
      ERR_INVALID_PRODUCT_ID: 6777012,
      ERR_LOAD: 6777002,
      ERR_LOAD_RECEIPTS: 6777004,
      ERR_MISSING_TOKEN: 6777016,
      ERR_PAYMENT_CANCELLED: 6777006,
      ERR_PAYMENT_EXPIRED: 6777020,
      ERR_PAYMENT_INVALID: 6777007,
      ERR_PAYMENT_NOT_ALLOWED: 6777008,
      ERR_PURCHASE: 6777003,
      ERR_REFRESH: 6777019,
      ERR_REFRESH_RECEIPTS: 6777011,
      ERR_SETUP: 6777001,
      ERR_SUBSCRIPTIONS_NOT_AVAILABLE: 6777015,
      ERR_UNKNOWN: 6777010,
      ERR_VERIFICATION_FAILED: 6777017,
      ERROR: 1,
      FINISHED: "finished",
      FREE_SUBSCRIPTION: "free subscription",
      inappbilling: { },
      INFO: 3,
      INITIATED: "initiated",
      INVALID: "invalid",
      INVALID_PAYLOAD: 6778001,
      log: { },
      NON_CONSUMABLE: "non consumable",
      OWNED: "owned",
      PAID_SUBSCRIPTION: "paid subscription",
      products: [ ],
      PURCHASE_EXPIRED: 6778003,
      QUIET: 0,
      REGISTERED: "registered",
      REQUESTED: "requested",
      sandbox: false,
      utils: { },
      VALID: "valid",
      verbosity: 4,
      WARNING: 2
   }
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: true,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "valid",
      title: "Consumable 5",
      transaction: null,
      type: "consumable",
      valid: true
   }

[store.js] DEBUG: store.queries !! 'consumable_5 requested'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable requested'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid requested'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'requested'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'updated'
eval code (14) (4,29)
product updated:  [object Object]
eval code (14) (4,29)
   "product updated: "
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: false,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "requested",
      title: "Consumable 5",
      transaction: null,
      type: "consumable",
      valid: true
   }

[store.js] DEBUG: store.queries !! 'consumable_5 initiated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable initiated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid initiated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'initiated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'updated'
eval code (14) (4,29)
product updated:  [object Object]
eval code (14) (4,29)
   "product updated: "
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: false,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "initiated",
      title: "Consumable 5",
      transaction: null,
      type: "consumable",
      valid: true
   }

InAppBilling[js]: buy called!
eval code (14) (4,29)
[store.js] DEBUG: windows -> product data for consumable_5
eval code (14) (4,29)
[store.js] DEBUG: {}
eval code (14) (4,29)
[store.js] DEBUG: undefined
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 approved'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable approved'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid approved'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'approved'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'updated'
eval code (14) (4,29)
product updated:  [object Object]
eval code (14) (4,29)
   "product updated: "
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: false,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "approved",
      title: "Consumable 5",
      transaction: { },
      type: "consumable",
      valid: true
   }

[store.js] DEBUG: verify -> true
eval code (14) (4,29)
[store.js] DEBUG: verify -> success: undefined
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 verified'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable verified'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid verified'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'verified'
eval code (14) (4,29)
[store.js] DEBUG: product -> defer finishing consumable_5
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'updated'
eval code (14) (4,29)
product updated:  [object Object]
eval code (14) (4,29)
   "product updated: "
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: false,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "approved",
      title: "Consumable 5",
      transaction: { },
      type: "consumable",
      valid: true
   }

[store.js] DEBUG: product -> finishing consumable_5
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable_5 finished'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable finished'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid finished'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'finished'
eval code (14) (4,29)
[store.js] DEBUG: plugin -> consumable finished
eval code (14) (4,29)
InAppBilling[js]: consumePurchase called!
eval code (14) (4,29)
Exception calling native with command :: InAppBillingPlugin :: consumePurchase ::exception=TypeError: Could not convert argument to type 'GUID'
eval code (14) (4,29)
Finished purchasing product! [object Object]
eval code (14) (4,29)
   "Finished purchasing product!"
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: false,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "finished",
      title: "Consumable 5",
      transaction: null,
      type: "consumable",
      valid: true
   }

[store.js] DEBUG: store.queries !! 'consumable_5 updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'consumable updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'valid updated'
eval code (14) (4,29)
[store.js] DEBUG: store.queries !! 'updated'
eval code (14) (4,29)
product updated:  [object Object]
eval code (14) (4,29)
   "product updated: "
   {
      [functions]: ,
      $$hashKey: "object:135",
      alias: "consumable_5",
      canPurchase: false,
      currency: "",
      description: "This is a fifth test consumable.",
      downloaded: false,
      downloading: false,
      id: "consumable_5",
      loaded: true,
      owned: false,
      price: "$0.00",
      state: "finished",
      title: "Consumable 5",
      transaction: null,
      type: "consumable",
      valid: true
   }

Thanks!

j3k0 commented 8 years ago

Windows 10 isn't supported (at least it hasn't been tested by any maintainer here). But it seems it's almost working, good news 👍

Did you find how to retrieve the transaction ID from...? It has to be around when whoever calls approved.

@dkarzon Any thoughts?

trumpet2012 commented 8 years ago

Yeah it does seem to almost work for consumable products in Windows 10. For durable products however it does not get as far. They fail immediately in the subscribe function. I will submit another issue for this.

The transaction ID does appear in the JSON information that gets passed to our server when it validates the order. So at some point the ID is known.

dkarzon commented 8 years ago

I thought I had it working with Windows 10 :/ It may have just been an 8.1 app running on Win10. Do you know what version of the plugin you are using? Also some sample code for us to double check? I don't think it will be a huge amount of work to fix, I'll see if what I can do when I get some time.

trumpet2012 commented 8 years ago

Here are the versions we are running for the relevant libraries and the plugin:

Below is the service we are using to interface with the plugin.

function StoreService($log, $window, $q, $http, $ionicPlatform, Anvil, APPLICATION_SETTINGS) {

    var product_registration = $q.defer();
    var purchasable_products = [];

    // TODO: Register callback with refresh to handle product restores.

    // Don't use the store until the plugins are ready.
    var store_promise = $ionicPlatform.ready().then(function () {
        $window.store.verbosity = $window.store.DEBUG;

        $window.store.validator = function(product, callback) {
            /**
             * This mirrors how the billing plugin (see {@link store._validator}) handles the request, except it uses 
             * the http service instead of an ajax call. This is done so that token authentication will be performed.
             * The plugin uses this function to validate the purchase is legitimate. Anvil will validate the purchase
             * with the store.
             *
             * See https://docs.angularjs.org/api/ng/service/$http for details about the http service response object.
             *
             * Response data (response.data) is expected to be in the format:
             * {
             *      "ok": true|false,
             *      "data": { ... }
             * }
             *
             * The data object can contain either an error code (i.e. if subscription is expired), or an error object.
             *
             * Billing plugin documentation:
             * https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#-storevalidator
             */
            $http({
                url: APPLICATION_SETTINGS.ANVIL_API_URL + '/v1/billing/validate/',
                method: 'POST',
                data: product,
                headers: {'Content-Type': 'application/json;charset=UTF-8'}
            }).then(
                function(response) {
                    var data = response.data;
                    callback(data && data.ok, data.data);
                },
                function(response) {
                    callback(false, 'Error ' + response.status + ': ' + response.data);
                }
            );
        };

        return $window.store;
    });

    function init_products(products) {
        console.log("product init", products);
        return store_promise.then(function (store) {
            store.error(function (error) {
                console.log("Error for store:", e.code, e.message);
            });

            // resolve product_registration if no products exist or once the first product is registered.
            if (products.length == 0) {
                $log.warn('StoreService: No products.');
                product_registration.resolve();
                return;
            }

            // convert product list to the right format and add them to the store
            var PRODUCT_TYPE_MAP = {
                'consumable': store.CONSUMABLE,
                'non consumable': store.NON_CONSUMABLE,
                'paid subscription': store.PAID_SUBSCRIPTION,
                'free subscription': store.FREE_SUBSCRIPTION,
                'non renewing subscription': store.NON_RENEWING_SUBSCRIPTION
            };
            console.log("products to be registered", products);
            angular.forEach(products, function (prod) {
                try {
                    store.register({
                        id: prod.product_id,
                        type: PRODUCT_TYPE_MAP[prod.product_type]
                    });
                } catch (e) {
                    $log.error("StoreService: Couldn't register product: ", prod, e);
                }
            });

            // This is only used when testing windows phone IAP
            $window.store.sandbox = APPLICATION_SETTINGS.WINDOWS_SANDBOX_IAP;

            // The docs label this as an expensive call so avoid using it extensively.
            $window.store.refresh();
        });
    }

    // Register all products with the store plugin 
    store_promise.then(function (store) {
        Anvil.Product.query().then(
            init_products,
            null,
            init_products
        );
    });

    // Initialize the store state machine.
    store_promise.then(function (store) {
        // The when("product") filter will match all products purchased.

        /* Update the list of purchasable products as they get registered with the store or OWNED */
        store.when("product").updated(function (product) {

            for (var i = purchasable_products.length - 1; i >= 0; i--) {
                if (purchasable_products[i].id == product.id) {
                    purchasable_products.splice(i, 1);
                }
            }
            console.log("product updated: ", product);
            if (product.canPurchase) {
                console.log("pushing to list", product);
                purchasable_products.push(product);
            }

            product_registration.resolve();
        });

        /* Customer has approved the purchase. Anvil should verify with the play store */
        store.when("product").approved(function (product) {
            product.verify();
        });
        /* Anvil has verified the purchase. Notify the app everything checks out. */
        store.when("product").verified(function (product) {
            product.finish();
        });
        /* Purchase successful. Grant access */
        store.when("product").finished(function (product) {
            //TODO: grant access
            console.log("Finished purchasing product!", product);
        })
    });

    /* Return the list if any of the products have been registered successfully. */
    var get_products = product_registration.promise.then(function () {
        console.log("Got products", purchasable_products);
        return purchasable_products;
    });

    /* Place an order with the store.  Callbacks walk the product through the order cycle.
     * The product life cycle is here:
     * https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#life-cycle */
    var place_order = function (product) {
        return store_promise.then(
            function (store) {
                console.log("Placing order: ", store, product);
                store.order(product);
            },
            function (error, arg1, arg2) {
                console.log("Error placing order: ",product, error, arg1, arg2);
            }
        );
    };

    return {
        get_products: get_products,
        place_order: place_order
    }
}