godot-sdk-integrations / godot-google-play-billing

Godot Android plugin for the Google Play Billing library
MIT License
147 stars 46 forks source link

purchases_updated(Object[].Purchase.list) does not return all purchases data made by the player #14

Open OlexiyKravchuk opened 4 years ago

OlexiyKravchuk commented 4 years ago

If the user buys the same product (SKU) in the store 3 or more times, the number of coins after payment is immediately consumed and credited to the player's internal account. But after that, the player cancels the first two purchases. And I could not find a way to determine what happened and how after that it handles such a situation in the application. I read the official documentation of Godot, there was no such information, there is a demo example, but it also does not have such an example, I also looked at the documentation from Google, but the Plugin provides a significantly reduced API, and I do not know JAVA well enough to understand what it is and where it is transferred, I have spent almost a month to understand everything, and ran into this question. So I don't see a way to find out using the transaction token that the transaction is no longer valid? Please, any example with clarification, because the fields of returned tributes through signals are not documented anywhere.

timoschwarzer commented 3 years ago

Hi!

I think this is more of a problem of the Google Play Billing library.

According to their docs, queryPurchases returns all owned, non-consumed items.

And here they say that you need to wait for the purchase to complete if you don't want to check on an external backend server.

Note: Because consumption requests can occasionally fail, you must check your secure backend server to ensure that each purchase token hasn't been used so your app doesn’t grant entitlement multiple times for the same purchase. Alternatively, your app can wait until you receive a successful consumption response from Google Play before granting entitlement. If you choose to withhold purchases from the user until Google Play sends a successful consumption response, you must be very careful not to lose track of the purchase after the consumption request.

I think the easiest solution would be waiting for your purchase status to become PURCHASED (= 1, https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState) before consumption.

OlexiyKravchuk commented 3 years ago

Hello! Thanks for the answer. And at the same time the problem is a little different ... The fact is that even when the purchase has already been paid for, it is then confirmed and consumed. And then, if copies of the same product are sold, then the token will no longer indicate previous purchases, but only the last purchase of a copy of this product. And if the buyer decides to withdraw those funds that he has already paid for some of the previous transactions except for the last one, then there is no mechanism in the application to find out about such an action, and I worry that this is a back door for abuse and cheating. I would like to clarify that I do not have a server for storing data, but even if I did have one, it didn’t secure the system from such actions, because there is no way to link the cancellation of payment and the state of the balance in the player’s account.

timoschwarzer commented 3 years ago

@OlexiyKravchuk this is then a problem with the Google Play Billing API and not with this plugin, isn't it?

OlexiyKravchuk commented 3 years ago

@timoschwarzer Yes I understand this. By the way. Here you say check status == 1 == PURCHASED but in this line #https://github.com/godotengine/godot-google-play-billing/blob/d5d5aaf4342b3afaec3f1b63d95622ff8624fdc2/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java#L196 it has a different meaning, or am I confusing something? However, I'm sure I'm confusing something because I'm already dizzy from switching all windows with JAVA code. and comparing all parameters ... This plugin needs very serious and detailed documentation, it is hellishly painful to work, relying on the source code of a language unknown to me, as if it were documentation.

And I found another problem, the Purchase class of the plugin is missing a few elements declared here ...

https://developer.android.com/reference/com/android/billingclient/api/Purchase

this is...

boolean equals (Object o) AccountIdentifiers getAccountIdentifiers () String getDeveloperPayload () String getOriginalJson () int hashCode () String toString ()

Maybe create a separate report for this?

OlexiyKravchuk commented 3 years ago

Oh I found what the confusion is ... PurchaseState != Status

OlexiyKravchuk commented 3 years ago

I tried to iterate over everything again today to organize my thoughts, it seems to me that it would be reasonable to rename the return value from "status" to ResponseCode as indicated in this document ...

https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult#getresponsecode

but adjusted for the internal style of Godot, it will turn out - "response_code" to correspond to the original meaning of the agreements and avoid such confusion as happened to me.

OlexiyKravchuk commented 3 years ago

No, what I wrote in the previous message is not correct! Because I rechecked the code for the hundredth time and next to "status", there is already a value for "response_code". O_o Again in this part of the code ...

https://github.com/godotengine/godot-google-play-billing/blob/d5d5aaf4342b3afaec3f1b63d95622ff8624fdc2/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java#L176

Ie it is absolutely not clear where did the value of "status" come from? Why is there such a construction?

It might be easier to just specify "response_code" and "debug_message" like this ...

    public Dictionary purchase(String sku) {
        if (!skuDetailsCache.containsKey(sku)) {
            Dictionary returnValue = new Dictionary();
            returnValue.put("response_code", null);
            returnValue.put("debug_message", "You must query the sku details and wait for the result before purchasing!");
            return returnValue;
        }

        SkuDetails skuDetails = skuDetailsCache.get(sku);
        BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
                                                   .setSkuDetails(skuDetails)
                                                   .build();

        BillingResult result = billingClient.launchBillingFlow(getActivity(), purchaseParams);

        Dictionary returnValue = new Dictionary();
        returnValue.put("response_code", result.getResponseCode());
        returnValue.put("debug_message", result.getDebugMessage());
        return returnValue;
    }

and that's enough?

OlexiyKravchuk commented 3 years ago

and here... https://github.com/godotengine/godot-google-play-billing/blob/d5d5aaf4342b3afaec3f1b63d95622ff8624fdc2/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java#L898

    public Dictionary queryPurchases(String type) {
        Purchase.PurchasesResult result = billingClient.queryPurchases(type);

        Dictionary returnValue = new Dictionary();
        returnValue.put("response_code", result.getBillingResult().getResponseCode());
        returnValue.put("debug_message", result.getBillingResult().getDebugMessage());
        returnValue.put("purchases", GooglePlayBillingUtils.convertPurchaseListToDictionaryObjectArray(result.getPurchasesList()));
        return returnValue;
    }
OlexiyKravchuk commented 3 years ago

I am quite a noob in JAVA so there may be errors in my optimization examples.

timoschwarzer commented 3 years ago

@OlexiyKravchuk status is an item of the Error enum (https://docs.godotengine.org/en/stable/classes/class_@globalscope.html)

OlexiyKravchuk commented 3 years ago

If it's not difficult for you, could you fix it? I mean that this status there is clearly superfluous and even harmful, it simply duplicates the "response_code" but at the same time it cuts down the information content of debugging and complicating the code is just confusing. As a result, it is not possible to receive a purchase request in the _on_connected () function, I just get a message that the request failed and fake code 1, instead of "debug_message" explaining why, and a valid "response_code" to handle the exception.

OlexiyKravchuk commented 3 years ago

By the way, there is a similar problem in the arguments passed to the onPurchasesUpdated (billingResult: BillingResult, purchases: List ?) Signal. It is not possible to handle exceptions in the plugin because the "BillingResult" argument containing "debug_message" and "response_code" is not passed to the plugin signal. I found this requirement in the documentation section here ...

https://developer.android.com/google/play/billing/integrate#launch

timoschwarzer commented 3 years ago

It is not possible to handle exceptions in the plugin

You can handle exceptions, actually. If the response code is not OK then the purchase_error signal will be fired with the response code and the debug message. (AFAIK the debug message is always empty for succeeded purchases, so it is not relevant for purchases_updated)

I mean that this status there is clearly superfluous and even harmful

No, for this simple reason: I wanted an error indicator for all methods that return a dictionary. There are cases where no response_code exists, and I wanted that Godot users can just look at status and see if it's an error or not. Also, this would break the API.

timoschwarzer commented 3 years ago

As a result, it is not possible to receive a purchase request in the _on_connected () function, I just get a message that the request failed and fake code 1, instead of "debug_message" explaining why, and a valid "response_code" to handle the exception.

You get those by subscribing to the connect_error signal.

ayenho commented 3 years ago

https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list

@timoschwarzer hi timoschwarzer thanks for the effort publishing this amazing plug-in. Although I took a couple of weeks to sort everything out. and I had a similar problem like the one @OlexiyKravchuk had.

Within this plug-in, I haven't found a way to check if end-users refund their purchase or not. I run a couple of test purchase myself, and even when I refund the order, the purchase_status from queryPurchases() remains 1 for weeks (likely forever).

The record will remain in the purchase history until I use consumePurchase() to purchase the item again. then I found this article and trying to learn and implement it into your plug-in.

just started, and haven't got any luck yet, but hope this helps.

again, thanks for the plug-in, it's a very good start to learn.