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

Product remains in "approved" state, even after order.finish() called #425

Closed trjdreyer closed 8 years ago

trjdreyer commented 8 years ago

I have implemented In-App purchasing using this plugin, at the moment specifically targeting Android, ie in-app purchases from the Google Play store. Everything seems to work correctly, ie I buy a (Managed, Non-Consumable) product that I have created on the Google Developer console for an App published for alpha testing. I call the store.order method passing in the relevant product id, the google In-App purchase popup appears, I confirm the purchase and the relevant amount gets deducted from my credit card. The store.when("product").approved listener picks up an event and if I inspect the given order I see that the state is "approved", the valid property is "true" and "owned" is false. The order contains a transaction, with type "android-playstore" and purchaseTime, receipt, purchaseToken, signature all populated as expected. The purchaseState is "0". In short, everything seems right and what I expected. The problem is that nothing much seems to happen when I call order.finish() on this order. I don't see any error messages in the log or anything, but when I open my application again, the store.when("product").approved event fires again for this product and the order seems exactly as it was before - state "approved" rather than "owned" as I expected and the "owned" property stil false.

Now, my question is, except for the inconvenience of this approved event firing every time, is there any problem with simply leaving the order in this state? The purchase amount was deducted from my credit card, if I look in google payments it shows the state of this purchase as "Confirmed" and in my Merchant account this transaction is shown as 'Charged'. Does this mean that the purchase has now succeeded for good, or is there a change that Google could still somehow reverse this transaction because the order.finish() didn't go through?

Also, any suggestion as to why I'm getting this behaviour with order.finish will of course also be much appreciated. I'm pretty sure I did everything as per the examples in the documentation.

I will be really glad for any feedback. Thanks!

caleb87 commented 8 years ago

Did you ever figure this out?

I have the same issue on both iOS and Android. Have you successfully finished an iOS order?

I see the finish() method is there when viewing the logged approved callback object (product in the API example), but it doesn't seem to be working.

AbuHani commented 8 years ago

try this

store.when("product").updated(function(product) {
            if(product.loaded && product.valid && product.state === store.APPROVED) {
               product.finish();
            }
}); 
trjdreyer commented 8 years ago

@AbuHani Thanks very much for the suggestion. So I added that code to my project. All the conditions are met, ie the updated method gets called, product.loaded is true, product.valid is true and product.state is store.APPROVED. The product.finish() method therefore gets called, but it doesn't seem to make any difference. There are no errors or anything. but the next time I open the application and get the product in question its state is still "approved" and not "owned", as I would have expected.. In short, I'm still having the issuee I had before.

trjdreyer commented 8 years ago

@AbuHani & @caleb87 If in the plugin, in the store-android.js file, I add the following lines to the end of the store.setProductData function, my application behaves as I expect it to behave (on Android, I'm not targeting iOS at this stage):

  if(data.purchaseState === 0){
    product.set("state", store.OWNED);
  }

With these lines added, the state of a given product becomes and remains "owned" rather than "approved" once product.finish() is called for that product.

Is this a valid fix? (because it seems to work .... ). If the data.purchaseState for a given product is 0, surely that means I own the product, does it not? (From the google documentation :purchaseState: The purchase state of the order. Possible values are 0 (purchased), 1 (canceled), or 2 (refunded)"

Or am I missing something?

accomplix commented 8 years ago

I'm using Play Store renewing "subscriptions", and I'm getting the same problem. The transaction succeeds, but order.finish() doesn't seem to change anything, so every time the app is started it tries to reprocess that subscription, and stays marked as "state: approved" instead of "state: owned".

caleb87 commented 8 years ago

trjdreyer, that solution is the same as checking if it's approved. Finish merely prevents that purchase from being restored. Sure it works to know that the payment is completed (which is the same as the approved state).

Apple Docs: "if your app fails to mark a transaction as finished, Store Kit calls the observer every time your app is launched until the transaction is properly finished". Android works the same. https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html

This is definitely a bug. This bug is also causing other issues: https://github.com/j3k0/cordova-plugin-purchase/issues/357

Unfortunately I don't know Java/Objective C, but I'll be learning some now. I've went through the JS code, but I didn't see anything yet.

On both iOS and Android, finish() does fire, changes the product to owned; however, when the app restarts all the items are restored and marked in the approved state. It will also cause a login prompt.

trjdreyer commented 8 years ago

@caleb87 Thanks very much for your reply.

So it seems there is clearly a bug here, and having to deal with those approved methods firing again every time the app starts up is a problem. But beyond these hassles, do you know what would ultimately happen - on Android - if finish() is never called for a purchase? Is it possible that Google will after enough time reverse such a transaction (that has already been charged on a user's credit card and for which a receipt was generated)? By reversing I mean returning the money to the user and changing the purchaseState to something other than 0

caleb87 commented 8 years ago

According to the plugin docs,

As long as you keep the product in state APPROVED:

the money may not be in your account (i.e. user isn't charged) you will receive the approved event each time the application starts, where you should try again to finish the pending transaction.

Have you tried working with other product types like consumables?

I'm testing consumables and subscriptions, but having trouble with those as well.

I've been looking for other Cordova/PG plugins for in app purchases, but it seems as though this is the only decent one... that doesn't work :(

trjdreyer commented 8 years ago

@caleb87 Thanks for your reply . Yes, it is definitely rather unfortunate that this plugin, which does seem to be the only decent one for in-app purchases, has this bug.

Re the quote from the plugin doc. Yes I did see that, and it is certainly in line with the apple docs (which you linked to before) that does indeed explicitly say that transactions have to be completed.

But, I'm not sure if I'm missing something, but I do not see anything comparable in the Android documentation. See the flow as described here.

http://developer.android.com/google/play/billing/api.html

I've also taken a look at some sample android code for doing in-app purchases and once again, maybe I'm missing something, but I can't see anything in it that looks to me like an explicit instruction to finish the transaction.

So my pet theory is that iOS and Android work differently in this regard, and that the bug in this plugin (ie the state remaining "approved" rather than becoming owned) is an annoyance in Android but will not prevent purchases from being successfully made and charged. I have a published app in the Google play store, that has been doing in-app purchases by means of this plug-in for a month, and everything seems fine (except of course for that 'approved' vs 'owned' business). But the (non-consumable) purchase is delivered, I get a receipt, the money is charged from the user's credit card, it reflects in my merchant account, and everything seems cool ... Also, when I try to buy a product with the same id again, I getf failure code 7 BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED, which is what I would expect ...

I have not deployed this app to the Apple Store, so I can't say anything about whether that works properly or not. Based on the apple docs referenced above, I seriously doubt that it would. But as I said before, I don't see anything about finishing in-app purchase transactions on Android ....

caleb87 commented 8 years ago

I believe you're correct.

Android you can consume items, but I don't see anything to mark all transactions as finished(). http://developer.android.com/training/in-app-billing/purchase-iab-products.html#Purchase

Because I'm supporting iOS, I decided to find a different plugin by Alex Disler. https://www.npmjs.com/package/cordova-plugin-inapppurchase

It worked perfectly for iOS; I actually prefer the design as well. Android I'm having issues, but I don't believe it's plugin related. If you really get in a pinch, perhaps that plugin will get you by.

Edit: Now have the other plugin working flawlessly for both iOS and Android.

remisture commented 8 years ago

Has anyone here live iOS apps with this behavior, but apparently still work as expected (money charged, money received)?

Dont think I will risk deploying to App Store with this bug, unless someone can confirm its actually working in production.

henkkelder commented 8 years ago

I have struggled with the same. It appears however this is working as it should. Please see:

https://github.com/j3k0/cordova-plugin-purchase/issues/295

The author of this component writes the following there:

This will be called each time your app starts for all purchases, allowing you to check if they're expired, fake or whatever (if you want to do those checks).

My "approved" handler basically checks if the non-consumable is already present and if so finishes the product. If the product is not present it is downloaded and then set to finished.

Also, both itunesconnect and Google Play Console report that purchases have been made.

AbuHani commented 8 years ago

hi guys, any one please help me on ios ? i created products but when purchase i got this error trying to purchase an unknown product !!

karuppasamypandian0856 commented 7 years ago
        store.order(alias).then(function(){console.log('success')}).error(function(err){console.log('error..'+err.code)});

        store.when(alias).approved(function (order) {
            order.finish();
        });

        store.when(alias).updated(function(p) {

                                  if(p.loaded && p.valid && p.state === store.APPROVED) {
                                  p.finish();
                                  }

                                  });
        store.trigger("refreshed");

        store.when(alias).finished(function(p){

                  if(iAPState == true)
                  {
                                   if ( device.platform == 'android' || device.platform == 'Android' )
                                       transid=p.transaction.purchaseToken;
                                   else
                                        transid=p.transaction.id;

                                   console.log(alias+" finished " + p.state + ", title is " + p.title+" "+JSON.stringify(p) );
                                   console.log('IAP for Tips called');
                                   iAPState=false;
                  }
                        });

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

        });

Its help to find

piin commented 7 years ago

Hi, I used your example @AbuHani : ) it seemed to work! Thanks!!!

ozcanavtat commented 5 years ago

My problem was remaining in "approved" state even after product.finish(). After testing all cases, I have figured it out by: store.when("product").updated(function(product) { if(product.loaded && product.valid && product.state === store.APPROVED && product. transaction!=null) { product.finish(); } });

I think this way is one and only way to set product state to "owned" because products transaction field is not null when product object (with state="approved") comes from updated event. If transaction field of product is null, product.finish() does not make sense.

hazmouneZaineb commented 3 years ago

I have a custom validation method configured for iOS, everything works fine, but if the validation failed with the code PURCHASE_EXPIRED or INTERNAL_ERROR, the product is still in the APPROVED state and does not return to the VALID state, so I cannot make a new purchase and it can't auto renew the subscription. How can i reset the product state ?

this.store.validator = async (product: IAPProduct, callback) => {
            const body: ApplePayValidateModel = {
                receipt: product.transaction.appStoreReceipt
            };
            try {
                const res = await this.service.Create(body, ApiUrl.VALIDATE_APPLE_PAY, RequestType.JSON).toPromise() as ServerResponse;
                const result = res.value as AppleValidation;
                if (result.passed) {
                    callback(true, { transaction: result.transaction });
                } else {
                    callback(false, {
                        code: result.status,
                        error: { message: "validation failed for " + product.id }
                    });
                }
            } catch (error) {
                callback(false, {
                    code: this.store.INTERNAL_ERROR,
                    error: { message: "error server => " + JSON.stringify(error) }
                });
            }
        };
this.store.when("subscription").unverified(p => console.log("unverified ", p));