j3k0 / cordova-plugin-purchase

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

No callbacks called when purchasing already purchased product. #447

Closed heaven closed 8 years ago

heaven commented 8 years ago

We have a non cunsumable product configured in iTunes, which I already bought. Now when I try to buy it again sometimes I see different messages:

In the first case everything works well, I click ok, it triggers callbacks and we process with a purchase, in the second case I click ok and nothing happens, no callbacks triggered, we can't restore anything and can't complete the purchase.

heaven commented 8 years ago

I can reproduce this:

> store.order("ERN_Lifetime")
< {then: function, error: function} = $5
[Log] [store.js] DEBUG: store.queries !! 'ERN_Lifetime requested' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'non consumable requested' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'valid requested' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'requested' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'ERN_Lifetime updated' (console-via-logger.js, line 173)
[Log] Store: updated – Product {id: "ERN_Lifetime", alias: "ERN_Lifetime", type: "non consumable", …} (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'non consumable updated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'valid updated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'updated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: ios -> is purchasing ERN_Lifetime (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'ERN_Lifetime initiated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'non consumable initiated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'valid initiated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'initiated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'ERN_Lifetime updated' (console-via-logger.js, line 173)
[Log] Store: updated – Product {id: "ERN_Lifetime", alias: "ERN_Lifetime", type: "non consumable", …} (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'non consumable updated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'valid updated' (console-via-logger.js, line 173)
[Log] [store.js] DEBUG: store.queries !! 'updated' (console-via-logger.js, line 173)

At this point it asks me to enter the password and then tells me that the purchase has already been made and the content will be restored for free. I click "Ok" and no any other events fired.

(I am testing on my phone, not in the emulator, but in the sandbox env)

heaven commented 8 years ago

img_2339

Currently there's no way to get out from this state, we can't even hide the spinner, so this is a severe issue.

j3k0 commented 8 years ago

You need to finish the purchase so the consumable gets consumed and you can purchase it again, see the appropriate section in the api doc.

heaven commented 8 years ago

I can't because none of callbacks are called, except the updated one. Which is not helpful in this case at all.

heaven commented 8 years ago

Here are our event handlers:

var setupProductHandlers = function(product) {
    store.when(product.id)
        .cancelled(function(p) {
            console.log("Store: cancelled", p);
            $ionicLoading.hide();
        });

    store.when(product.id)
        .unverified(function(p) {
            console.log("Store: unverified", p);
            $ionicLoading.hide();
        });

    store.when(product.id)
        .loaded(function(p) {
            console.log("Store: loaded", p);
        });

    store.when(product.id)
        .approved(function(p) {
            console.log("Store: approved", p);
            p.verify();
        });

    store.when(product.id)
        .verified(function(p) {
            console.log('Store: verified', p);
            p.finish();
        });

    store.when(product.id)
        .error(function(p) { console.log("Store: error", p) });

    store.when(product.id)
        .updated(function(p) { console.log("Store: updated", p) });

    store.when(product.id)
        .owned(function(p) { console.log("Store: owned", p) });

    store.when(product.id)
        .finished(function(p) { console.log("Store: finished", p) });

    store.when(product.id)
        .expired(function(p) { console.log("Store: expired", p) });
};

And in another place we have:

store.validator = function(p) {
    console.log('Store: validator', p);
    return $rootScope.$emit('subscription', p);
};

When registering products on android it fires owned events and we can restore all the owned products, this doesn't happen on ios.

heaven commented 8 years ago

According to the doc you showed me (https://developer.apple.com/in-app-purchase/In-App-Purchase-Guidelines.pdf) non consumable purchases should be restored on user devices, but there's no way to do this on ios. Seems like a bug.

UPD. Btw, when registering the products during the app startup it shows:

[store.js] DEBUG: ios -> product ERN_Lifetime is valid (ERN_Lifetime)
[store.js] DEBUG: ios -> owned? false
...
[store.js] DEBUG: ios -> product ERN_Subscription is valid (EatRightNow DTC Subscription)
[store.js] DEBUG: ios -> owned? false

But then I try to buy the lifetime plan and it says it is already purchased.

j3k0 commented 8 years ago

Your verify() method isn't valid, so after .approved calls verify, the plugin will not know that the purchase was successfully validated, thus never finish the purchase.

For now, you can't purchase anymore because there's a purchase stuck in the queue that needs handling. It's possible that you register your handlers after the initial refresh()... If that so, you'll never be able to handle pending purchases.

On Android the behavior is different, because transaction statuses are handled internally by the SDK (which isn't the case on iOS). If you got your code right, it should work on both platform. Non-consumable are well tested (it's supported by the plugin since its first version).

Anyway, I'm not 100% sure what exactly is causing your issue, can you share the whole logs (from the beginning)?

heaven commented 8 years ago

Hi,

  1. The .approved callback is never called when I try to buy same product second time.
  2. This error happens even for new accounts. I've tried many times to create new sandbox accounts, first time the purchase works great, all callbacks triggered as expected. But when I try to purchase the same thing once more – no store callbacks triggered and I can't restore the purchase. We register the handlers before the refresh call.
  3. It does work well on android, on ios only subscriptions work well, the described problem is valid only for non consumable products (we tried only non consumable and subscriptions).
  4. Here are the entire log output since the startup and until we get the popup from the screenshot above: 4.1 first attempt (successful): http://pastie.org/pastes/10840938/text?key=vcsvedq6ga7iff3abi03bw 4.2 second attempt (failed): http://pastie.org/pastes/10840887/text?key=4hlqpctgjta61dwpeybywg 4.3 our payments service: http://pastie.org/private/zvo2rrcltlkk5flryb3osw

The logs are for a new sandbox account.

UPD. And you should be right about unfinished/improperly finished purchases, because now my phone asks me for passwords of all those sandbox accounts I used for testing, and this is just hell :) I know such thing happens when there are not finished purchases or something like that.

j3k0 commented 8 years ago

(first attempt, 4.1). Approved is called. But from what I see the purchase is not finished. I would expect to see validation, then a call to verified, then a change of state to finish. Your implementation of the validator method seems wrong, you're never calling the callback: please see https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#-storevalidator

(second attempt, 4.2). Since there's a pending transaction, you should be able to process it when the app starts, I don't see anything related to that in the log. Since the validator in 4.1 neither failed nor succeeded (callback not called), you may have put the finger on a bug that is caused by this first issue (that virtually nobody should ever fall into if you solve point 4.1).

j3k0 commented 8 years ago

Also, the code handling non-processed consumable purchase may need to be checked for bugs. I think that most people just auto-finish consumable purchases (or, in practice, nothing fails between purchase and delivery), so they may not have encountered this case.

Let me know!

heaven commented 8 years ago

Hi, you were completely right, the problem was our validator. But this showed a problem with the plugin, e.g. if the app crash or something went wrong during the purchase process and the p.finish() wasn't called users wouldn't be able to make further purchases. There should be a way to resolve this and the plugin should trigger anything after I clicked the "Ok" button in the popup above.

Also, the code handling non-processed consumable purchase may need to be checked for bugs.

The problem is with non-processed non consumable purchases, but subscriptions may also stay unfinished and also not asked to be processed after the app starts.

j3k0 commented 8 years ago

You're totally right about the unfinished non consumable issue, this has to be handled.

Except if you prove me wrong, I think unfinished subscription are fine. I've just had that case this week for a project I'm working on and it worked as expected.

heaven commented 8 years ago

All I can say about the unfinished subscriptions is that they at least don't stuck in the queue and users can purchase these again and again.

I am not sure though if the user will be charged, but it seems it should, because subscriptions live as this is described by apple and get renewed every 5 minutes for about six times, and then die.

I am not sure if unfinished subscriptions would cause spamming of these password requests that I observe now, I used about 5 or more accounts for testing and still receive these password requests every time I start the app. I don't know if these triggered by unfinished non consumables or by subscriptions too, because I tried to purchase both. Easy to check though :)

j3k0 commented 8 years ago

Thanks for all the details.

I'm closing this issue for now and create a more specific one. Feel free to add more if you learn anything, I'll get notified.

heaven commented 8 years ago

I found 2 other issues tha I am not certain about:

I can create a separate issues if these aren't known issues or are okay to happen.

j3k0 commented 8 years ago

For the password on startup, if there aren't any unfinished transaction on the queue, it may be caused by revalidation of the subscription, which require accessing the appStoreReceipt, which require the user's password. You can try removing the validation step to make sure of that.

If that's your issue and you don't want that, you can store somewhere (on the device or the server) the subscription expiry, so you don't enable receipt validation when you know the subscription is still valid.

"application data" provides information present on the appStoreReceipt about the app itself. Original purchase date is sometime useful, if you want certain features to stay unlocked for people having downloaded the app before a given date. I think the product id has to be the application id or something (it's been a while I didn't use that).

heaven commented 8 years ago

It asks not only password but to sign in to itunes (asks for email too), before I register anything (even if I would not register any products and call the store.refresh()). Perhaps that's related to those unfinished purchases somehow, but for them it only asks for password, so it is weird.

j3k0 commented 8 years ago

Did you make sure you logged out your normal iTunes / AppStore account from the iPhone Settings? Maybe it's because you're still logged in globally on the phone with a non-sandbox account (that is tried first, and is the only one remembered).

zafercelik commented 7 years ago

I'm getting an error when I buy my product, I pay for the product, but after the successful payment screen I encounter this error. I get Error 6777013 error when I click on the product for a second time. I use devextreme

Plugin xml

Source Codes: function payment() {

var product_id = "com.developer.appname.product";

if (!window.store) {
    alert('Store not available');
    return;
}

store.verbosity = store.DEBUG;

// A consumable 100 coins product
store.register({
    id: product_id,
    alias: product_id,
    type: store.CONSUMABLE
});

// When any product gets updated, its details are passed to your app
store.when(product_id).updated(function (p) {
    // This is a good place to prepare or render the UI based on these refreshed details:
    if (p.valid) {
        var productId = p.id; // call store.order(productId) to buy this product
        var title = p.title;
        var description = p.description;
        var canPurchase = p.canPurchase;
        var price = p.price; // in the currency of the users App Store account
    }

});

store.when(product_id).verified(function (p) {

    p.finish();
    alert(" Verified Finis ");
});   

store.when(product_id).unverified(function (p) {
        alert("subscription unverified");
        });

// When the purchase of 100 coins is approved, show an alert
store.when(product_id).approved(function (order) {

    order.finish();
    alert("OK.");
});

// When the store is ready all products are loaded and in their "final" state.
store.ready(function () {
    alert("The store is ready");
});

// Deal with errors:
// -----------------

store.error(function (error) {
    alert('ERROR ' + error.code + ': ' + error.message);
});

// As the last step, refresh the store:
// -------------------------------------

// This will contact the server to check all registered products validity and ownership status.
// It's mostly fine to do this only at application startup but you can refresh it more often.
store.refresh();
}