libgdx/gdx-pay

How to detect a cancelled order? Also how do you acknowledge/consume a purchase?

FF7Squirrelman opened this issue · 15 comments

I've been using a fairly outdated gdx-pay and recently noticed that my cancelled orders code doesn't work. so I updated gdx-pay to 0.9.0 (tried 1.1.0 but came up as "Failed to resolve"). But am still using code based on a very old example (https://bitbucket.org/just4phil/gdxpayexample/src/master/android/), I tried reading through the example on the gdx-pay github but it's fairly confusing to me and the one I linked seems to work as purchases are working fine.

The only real problem is I can't seem to detect cancelled orders. I purchased a order using a tester account and then went to my google play console and refunded it with the revoke checkbox checked. But in my code I am expecting the following to return false if the order was cancelled, instead the transaction ceases to exit at all.

public PurchaseObserver purchaseObserver = new PurchaseObserver()
    {
        @Override
        public void handleRestore (Transaction[] transactions)
        {
            for (int i = 0; i < transactions.length; i++)
            {
                if(transactions[i].getIdentifier().equalsIgnoreCase("removeads") == true)
                {
                    if(transactions[i].isPurchased() == true)
                    {
                        myInterface.getRemoveAdsCheckbox().setText("X");
                    }
                    else
                    {
                        myInterface.getRemoveAdsCheckbox().setText("");
                    }
                }`
            }
        }
    }

There's also handlePurchaseCanceled () in the purchaseobserver but it never gets called. I thought of a work around where I can check if a transaction exists for each iap and if it doesn't then it's cancelled but I don't know if that would have unforseen side effects?

another seperate question (should I create a seperate issue for this? I am a bit unfamiliar with github practices). I recently read about the google billing 2.0 changes requiring us to acknowledge purchases within 3 days or they are autorefunded, does gdx-pay do this already or is it using an older version of google billing? aka does this effect gdx-pay users in anyway? and if we did want to consume a purchase how do we go about doing that?

The platform is google play. The module I am not 100% sure about since I set it up a long time ago but I am pretty sure its the gdx-pay-android module. my gradle imports are:

allprojects
gdxPayVersion = '0.9.0'

project(":android")
implementation "com.badlogicgames.gdxpay:gdx-pay-android:$gdxPayVersion"
implementation "com.badlogicgames.gdxpay:gdx-pay-android-googleplay:${gdxPayVersion}@aar"

project(":core")
implementation "com.badlogicgames.gdxpay:gdx-pay-client:$gdxPayVersion"

and this is my GooglePlayResolver (in case its still not obvious which module I am using since I don't remember what the gdx-pay-client import is).

package com.binaryblade.android;

import com.badlogic.gdx.pay.PurchaseManagerConfig;
import com.binaryblade.GameWorld;
import com.binaryblade.IdlePortalDefense;
import com.binaryblade.PlatformSpecific.PlatformResolver;

public class GooglePlayResolver extends PlatformResolver
{
    private final static String GOOGLEKEY = "REMOVED";

    static final int RC_REQUEST = 10001;	// (arbitrary) request code for the purchase flow

    public GooglePlayResolver(IdlePortalDefense game, GameWorld world)
    {
        super(game);

        PurchaseManagerConfig config = world.purchaseManagerConfig;
        config.addStoreParam(PurchaseManagerConfig.STORE_NAME_ANDROID_GOOGLE, GOOGLEKEY);
        initializeIAP(null, world.purchaseObserver, config);
    }
}

You should use last release of gdx-pay, with gdx-pay-android-googlebilling and not any other (old) unsupported gdx-pay android module.

gdxpay:gdx-pay-android-googleplay is no longer supported, also uses a Google API that is soon to be removed.

Alright, I didn't realize the angroid-google play got replaced by android-googlebilling. I replaced

implementation "com.badlogicgames.gdxpay:gdx-pay-android-googleplay:${gdxPayVersion}@aar"

with

implementation "com.badlogicgames.gdxpay:gdx-pay-android-googlebilling:$gdxPayVersion"

and increase the gdx-pay version to 1.1.0. I removed
implementation "com.badlogicgames.gdxpay:gdx-pay-android:$gdxPayVersion"
but kept
implementation "com.badlogicgames.gdxpay:gdx-pay-client:$gdxPayVersion"

But now it's giving me the error "cannot access PurchaseManager
class file for com.badlogic.gdx.pay.PurchaseManager not found" even though I can left click and go to its declaration and its there in package com.badlogic.gdx.pay

I keep reading through the main gdx-pay page and the gdx-pay-android-googlebilling page to see if I missed any steps but i'm not seeing anything so far. The only thing I can think of so far is that I might have done the proguard wrong but I added "-keep class com.android.vending.billing.**" and it seemed pretty straightforward how to add it.

Also the new module doesn't use the GooglePlayResolver or PlatformResolver classes anymore right? I'm not actually sure if they were ever in the official gdx-pay instructions or if they were something just in the example by just4phil that I linked before.

Are you using PurchaseSystem class? I believe you should not use that class anymore, but instead instantiate purchasemanager yourself

I was using PurchaseSystem with the old modules but not after updating. After updating I have:
purchaseManager.install(purchaseObserver, pmc, true);
in one of my constructors and:
world.purchaseManager = new PurchaseManagerGoogleBilling(this);
in my android launcher (it's not actually in the oncreate because of how I have my code setup but I tried it the suggested way and that didn't work either anyway). So I think i'm doing everything required to instantiate purchasemanager. As well as the code that adds the iap of course. I'll have to go through my code and post a more complete simplified summary of how I set it up but I can't tonight.

Alright I finally had time to simplify the code, I tried to take most of the irrelevent stuff out but I left some that I wasn't 100% sure about. Oh and I did see a similiar past issue that was caused by using eclipse instead of android studio so I should mention I am using android studio.

proguard-rules.proguard-rules

-verbose

-dontwarn android.support.**
-dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication
-dontwarn com.badlogic.gdx.utils.GdxBuild
-dontwarn com.badlogic.gdx.physics.box2d.utils.Box2DBuild
-dontwarn com.badlogic.gdx.jnigen.BuildTarget*
-dontwarn com.badlogic.gdx.graphics.g2d.freetype.FreetypeBuild

-keep class com.badlogic.gdx.controllers.android.AndroidControllers

-keepclassmembers class com.badlogic.gdx.backends.android.AndroidInput* {
   <init>(com.badlogic.gdx.Application, android.content.Context, java.lang.Object, com.badlogic.gdx.backends.android.AndroidApplicationConfiguration);
}

-keepclassmembers class com.badlogic.gdx.physics.box2d.World {
   boolean contactFilter(long, long);
   void    beginContact(long);
   void    endContact(long);
   void    preSolve(long, long);
   void    postSolve(long, long);
   boolean reportFixture(long);
   float   reportRayFixture(long, float, float, float, float, float);
}

-keep class com.android.vending.billing.**
-keep class com.amazon.** {*;}
-keep class com.sec.android.iap.**
-keep class com.nokia.payment.iap.aidl.**
-keep class com.badlogic.gdx.pay.android.** { *; }
-keep class com.android.vending.billing.**
-dontwarn org.onepf.oms.appstore.FortumoBillingService

main build.gradle

buildscript {
    

    repositories {
        mavenLocal()
        mavenCentral()
        maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "https://jcenter.bintray.com" }
        jcenter()
        google()
    }
    dependencies {
        classpath 'de.richsource.gradle.plugins:gwt-gradle-plugin:0.6'
        classpath 'com.android.tools.build:gradle:3.2.0'
        classpath 'com.mobidevelop.robovm:robovm-gradle-plugin:2.3.1'

    }
}

allprojects {
    apply plugin: "eclipse"
    apply plugin: "idea"

    version = '1.0'
    ext {
        appName = "IdlePortalDefense"
        gdxVersion = '1.9.8'
        roboVMVersion = '2.3.5'
        box2DLightsVersion = '1.4'
        ashleyVersion = '1.7.0'
        aiVersion = '1.8.0'
        gdxPayVersion = '1.1.0'
    }

    repositories {
        mavenLocal()
        mavenCentral()
        jcenter()
        maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "https://oss.sonatype.org/content/repositories/releases/" }
        maven { url "https://maven.google.com" }
        maven { url "https://jcenter.bintray.com" }
        google()
    }
}

project(":desktop") {
    apply plugin: "java"


    dependencies {
        implementation project(":core")
        implementation "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion"
        implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
        implementation "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-desktop"
        implementation "com.badlogicgames.gdx:gdx-tools:$gdxVersion"
        
    }
}

project(":android") {
    apply plugin: "android"

    configurations { natives }

    dependencies {
        implementation project(":core")
        implementation "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
        natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi"
        natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a"
        natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a"
        natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86"
        natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64"
        implementation "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
        natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi"
        natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi-v7a"
        natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-arm64-v8a"
        natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-x86"
        natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-x86_64"
        implementation "com.google.android.gms:play-services-games:15.0.1"
        implementation "com.google.android.gms:play-services-auth:15.0.1"
        implementation "com.google.android.gms:play-services-ads:15.0.0"
        implementation project(':BaseGameUtils')
        implementation "com.badlogicgames.gdxpay:gdx-pay-android-googlebilling:${gdxPayVersion}@aar"
    }
}

project(":ios") {
    apply plugin: "java"
    apply plugin: "robovm"


    dependencies {
        implementation project(":core")
        implementation "com.mobidevelop.robovm:robovm-rt:$roboVMVersion"
        implementation "com.mobidevelop.robovm:robovm-cocoatouch:$roboVMVersion"
        implementation "com.badlogicgames.gdx:gdx-backend-robovm:$gdxVersion"
        implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-ios"
        implementation "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-ios"
        
    }
}

project(":core") {
    apply plugin: "java"


    dependencies {
        implementation "com.badlogicgames.gdx:gdx:$gdxVersion"
        implementation "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
        implementation "com.badlogicgames.gdxpay:gdx-pay-client:$gdxPayVersion"
    }
}

tasks.eclipse.doLast {
    delete ".project"
}

GameWorld (not my main game class like the readme recommends but it's worked this way in the past with gdx-pay and I even tried switching it to the recommended way and it made no difference)

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.RandomXS128;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.pay.Offer;
import com.badlogic.gdx.pay.OfferType;
import com.badlogic.gdx.pay.PurchaseManager;
import com.badlogic.gdx.pay.PurchaseManagerConfig;
import com.badlogic.gdx.pay.PurchaseObserver;
import com.badlogic.gdx.pay.Transaction;
import com.badlogic.gdx.utils.Array;
import com.binaryblade.PlatformSpecific.ActionResolver;
import com.binaryblade.PlatformSpecific.ActionResolver.*;
import java.util.ArrayList;

public class GameWorld
{
	public ActionResolver actionResolver;	
	
	// ----- app stores -------------------------
        public static final int APPSTORE_UNDEFINED	= 0;
        public static final int APPSTORE_GOOGLE 	= 1;
        public static final int APPSTORE_OUYA 		= 2;
        public static final int APPSTORE_AMAZON 	= 3;
        public static final int APPSTORE_DESKTOP 	= 4;

        private int isAppStore = APPSTORE_GOOGLE;

	public final static String productID_removeads = "removed";

	public PurchaseManager purchaseManager;

	public GameWorld(IdlePortalDefense game, ActionResolver actionResolver, IdlePortalDefense idlePortalDefense)
	{
		this.game = game;
        this.actionResolver = actionResolver;

        // ---- IAP: define products ---------------------
        PurchaseManagerConfig pmc = new PurchaseManagerConfig();
        pmc.addOffer(new Offer().setType(OfferType.ENTITLEMENT).setIdentifier(productID_removeads));

		pmc.addStoreParam("testName", "testparam");//storename, param);
        purchaseManager.install(new purchaseObserver(), pmc, true);
		
        actionResolver.initializePlatformResolver(this);	
	}
	
	public class purchaseObserver implements PurchaseObserver
        {
            @Override
            public void handleInstall()
            {
                Gdx.app.log("IAP", "Installed");

                Gdx.app.postRunnable(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        System.out.println("handleInstall run");
                    }
                });
            }

            @Override
            public void handleInstallError(final Throwable e)
            {
                Gdx.app.error("IAP", "Error when trying to install PurchaseManager", e);
                Gdx.app.postRunnable(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        System.out.println("handleInstallError run");
                    }
                });
            }

            @Override
            public void handleRestore(final Transaction[] transactions)
            {
                if (transactions != null && transactions.length > 0)
                {
                    for (Transaction t : transactions)
                    {
                        handlePurchase(t, true);
                    }
                }
            }

            @Override
            public void handleRestoreError(Throwable e)
            {
                System.out.println("handleInstallError = " + e);
            }

            @Override
            public void handlePurchase(final Transaction transaction)
            {
                handlePurchase(transaction, false);
            }

            protected void handlePurchase(final Transaction transaction, final boolean fromRestore)
            {
                Gdx.app.postRunnable(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        if (transaction.isPurchased())
                        {
                            if (transaction.getIdentifier().equals(productID_removeads))
                            {
                                //do stuff here
                            }
                        }
                    }
               });
            }

            @Override
            public void handlePurchaseError(Throwable e)
            {
                showErrorOnMainThread("Error on buying:\n" + e.getMessage());
            }

            @Override
            public void handlePurchaseCanceled()
            {

            }

            private void showErrorOnMainThread(final String message)
            {
                Gdx.app.postRunnable(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        // show a dialog here...
                    }
                });
            }
        }
    }

AndroidLauncher

import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.Toast;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import com.badlogic.gdx.pay.android.googlebilling.PurchaseManagerGoogleBilling;
import com.badlogic.gdx.utils.Base64Coder;
import com.binaryblade.GameWorld;
import com.binaryblade.IdlePortalDefense;
import com.binaryblade.PlatformSpecific.ActionResolver;
import com.binaryblade.PlatformSpecific.Interface;
import com.binaryblade.R;
import com.google.android.gms.ads.AdListener;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.InterstitialAd;
import com.google.android.gms.ads.MobileAds;
import com.google.android.gms.ads.reward.RewardItem;
import com.google.android.gms.ads.reward.RewardedVideoAd;
import com.google.android.gms.ads.reward.RewardedVideoAdListener;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.auth.api.signin.GoogleSignInResult;
import com.google.android.gms.drive.Drive;
import com.google.android.gms.games.Games;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;

public class AndroidLauncher extends AndroidApplication implements ActionResolver, RewardedVideoAdListener
{
	IdlePortalDefense game;

	@Override
        protected void onCreate(Bundle savedInstanceState)
        {
	    super.onCreate(savedInstanceState);
	    AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
            game = new IdlePortalDefense(this, myInterface);
	}
	
	public void initializePlatformResolver(GameWorld world)
        {
            if (world.getAppStore() == GameWorld.APPSTORE_GOOGLE)
            {
                world.purchaseManager = new PurchaseManagerGoogleBilling(this);
            }
       }
}

The error occurs in the androidlauncher at:
world.purchaseManager = new PurchaseManagerGoogleBilling(this);

This seems unusual since theres a reference to purchaseManager that compiles fine before the above call the line:
purchaseManager.install(new purchaseObserver(), pmc, true);

which makes me think maybe somehow the android module can't see the purchaseManager class but the core one can?

Edit: Fixed typos and made the code collapsible for readability.

Please let us know what the error/compile message is.

Sorry it got kind of lost in my earlier posts.

error: cannot access PurchaseManager
class file for com.badlogic.gdx.pay.PurchaseManager not found

Did you try to declare the core dependency in your android subproject?

Alright, I had implementation project(":core") in my main gradle android dependencies but I tried adding implementation "com.badlogicgames.gdxpay:gdx-pay-client:$gdxPayVersion" to the android dependencies as well, though I think the readme just said to add it to the core dependencies. That seemed to fix the PurchaseManager not found issue. I had to adjust some of my other code as well since some of it didn't work the same with the latest version but it seems to be working now as it sees my previous purchases and runs without any errors. But I am still unsure about my initial questions as updating to the latest version didn't seem to change those issues.

I'l repeat them here:

  1. How do I detect cancelled orders? I cancelled a purchase with the revoke box checked and it no longer shows in my transactions, I was under the impression that to detect a cancelled order you check transactions[i].isPurchased() and if it returns false then the order was cancelled, but instead the transaction doesn't have a record at all anymore. I also have handlePurchaseCanceled in my purchaseobserver but it never gets called either.

  2. How do we consume a purchase?

  3. I recently read about the google billing 2.0 changes requiring us to acknowledge purchases within 3 days or they are autorefunded, does gdx-pay do this already without any additional code or is it using an older version of google billing? aka does this effect gdx-pay users in anyway?

  1. That's the behaviour of Google Billing. A cancelled order is not returned any more.
    handlePurchaseCanceled is called by some billing services if a purchase flow is cancelled.

  2. They are consumed automatically if it is a consumable item.

  3. This lib still uses an older version. When we update to 2.0, there will be no changes required by games because the acknowledging can take place in the lib.

So there is no way to revoke an iap at all? That seems unusual, couldn't users just abuse the playstore refund policy to buy iap and then immediatley refund them and keep the iap? Is there any reason not to detect them the way I mentioned in my initial post (if there is no longer a transaction record then the item was refunded and reverse their purchase)? I guess that might not be able to distinguish between a cancelled order and a refunded order (where a cancelled order would revoke the item and a refund would most likely let them keep the item).

Something like this:

removeAdsTransaction = false;

if (transactions != null && transactions.length > 0)
{
    for (int i = 0; i < transactions.length; i++)
    {
        if(transactions[i].getIdentifier().equalsIgnoreCase("removeadvertisements") == true)
        {
            if(transactions[i].isPurchased() == true)
            {
                removeAdsTransaction = true;
            }
       }
}

if(removeAdsPurchased == true) //this would be loaded from a file
{
    if(removeAdsTransaction == false)
    {
        //order canceled, reverse purchase award
    }
}

Not on the client, if you want 100% security you need to use your own backend and the server api to validate the transaction.

Ah, I mean i'm not concerned about the occasional person getting through that knows how to break the system as all my purchases are limited to 1 purchase and usually aren't pay to win style iap. More of mass abuse from normal people. I updated my last reply to include a code example of what I meant. But it sounds like that wouldn't work at all? so i'll close the issue. Thanks for the help.