How to best save InApp purchase status locally?

2020-02-16 07:00发布

问题:

I'm on the edge of finishing my first app, and one last remaining thing is to implement IAP billing, so that's why I am currently reading quite a lot about the topic (including security concerns like encryption, obfuscation and stuff).

My app is a free version, with the ability to upgrade to full verison via IAP, so there would be just one managed purchase item "premium". I have a few questions about this:

In the Google IAP API example (trivialdrivesample), there's always the IAP check in MainActivity to see if the user bought the premium version, done via

mHelper.queryInventoryAsync(mGotInventoryListener);

My first concern: This does mean that the user always needs to have an internet/data connection at app-startup, to be able switch to the premium version right? What if the user doesn't have an internet connection? He would go with the lite version I guess, which I would find annoying.

So I thought about how to save the isPremium status locally, either in the SharedPrefs or in the app database. Now, I know you can't stop a hacker to reverse engineer the app, no matter what, even so because I don't own a server to do some server-side validation.

Nevertheless, one simply can't save an "isPremium" flag somewhere, since that would be too easy to spot.

So I was thinking about something like this:

  • User buys Premium
  • App gets the IMEI/Device-ID and XOR encodes it with a hardcoded String key, saves that locally in the app database.

Now when the user starts the app again:

  • App gets encoded String from database, decodes it and checks if decodedString == IMEI. If yes -> premium
  • If no, then the normal queryInventoryAsync will be called to see if the user bought premium.

What do you think about that approach? I know it's not supersecure, but for me it's more important that the user isn't annoyed (like with mandatory internet connection), than that the app will be unhackable (which is impossible anyway). Do you have some other tips?

Another thing, which I currently don't have a clue about, is how to restore the transaction status when the user uninstalls/reinstalls the app. I know the API has some mechanism for that, and aditionally my database can be exported and imported through the app (so the encoded isPremium flag would be exportable/importable as well). Ok, I guess that would be another question, when the time is right ;-)

Any thoughts and comments to this approach are welcome, do you think that's a good solution? Or am I missing something/heading into some wrong direction?

回答1:

I too was making the same investigations, but during my testing I figured out that you do not need to store it, as Google do all the caching you need and I suspect (though I have not investigated it) that they are doing so as securely as possible (seeing as it in their interest too!)

So here is what i do

// Done in onCreate
mHelper = new IabHelper(this, getPublicKey());

mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
      if (!result.isSuccess()) {
         // Oh noes, there was a problem.
         Log("Problem setting up In-app Billing: " + result);
      } else {
         Log("onIabSetupFinished " + result.getResponse());
         mHelper.queryInventoryAsync(mGotInventoryListener);
     }
    }
});

// Called by button press
private void buyProUpgrade() {
    mHelper.launchPurchaseFlow(this, "android.test.purchased", 10001,   
           mPurchaseFinishedListener, ((TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId());
}

// Get purchase response
private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) 
    {
       if (result.isFailure()) {
          Log("Error purchasing: " + result);
          return;
       }      
       else if (purchase.getSku().equals("android.test.purchased")) {
      Log("onIabPurchaseFinished GOT A RESPONSE.");
              mHelper.queryInventoryAsync(mGotInventoryListener);
      }
    }
};

// Get already purchased response
private IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result,
       Inventory inventory) {

       if (result.isFailure()) {
         // handle error here
           Log("Error checking inventory: " + result); 
       }
       else {
         // does the user have the premium upgrade?        
         mIsPremium = inventory.hasPurchase("android.test.purchased");        
         setTheme();

         Log("onQueryInventoryFinished GOT A RESPONSE (" + mIsPremium + ").");
       }
    }
};

So what happens here?

The IAB is set up and calls startSetup, on a successful completion (as long as it has been run once with an internet connection and is set up correctly it will always succeed) we call queryInventoryAsync to find out what is already purchased (again if this has been called while online it always works while offline).

So if a purchase is completed successfully (as can only be done while online) we call queryInventoryAsync to ensure that it has been called while online.

Now there is no need to store anything to do with purchases and makes your app a lot less hackable.

I have tested this many ways, flight mode, turning the devices off an on again and the only thing that messes it up is clearing data in some of the Google apps on the phone (Not likely to happen!).

Please contribute to this if you have different experiences, my app is still in early testing stage.



回答2:

I refactored ne0's answer into a static method, including the comments from snark.

I call this method when my app starts - you'll need to enable your features at the TODO

/**
 * This is how you check with Google if the user previously purchased a non-consumable IAP
 * @param context App Context
 */
public static void queryPlayStoreForPurchases(Context context)
{
    final IabHelper helper = new IabHelper(context, getPublicKey());

    helper.startSetup(new IabHelper.OnIabSetupFinishedListener() 
    {
         public void onIabSetupFinished(IabResult result) 
         {
               if (!result.isSuccess()) 
               {
                   Log.d("InApp", "In-app Billing setup failed: " + result);
               } 
               else 
               {  
                    helper.queryInventoryAsync(false, new IabHelper.QueryInventoryFinishedListener()
                    {
                        public void onQueryInventoryFinished(IabResult result, Inventory inventory)
                        {
                            // If the user has IAP'd the Pro version, let 'em have it.
                            if (inventory.hasPurchase(PRO_VERSION_SKU))
                            {
                                //TODO: ENABLE YOUR PRO FEATURES!! 

                                Log.d("IAP Check", "IAP Feature enabled!");
                            }
                            else
                            {
                                Log.d("IAP Check", "User has not purchased Pro version, not enabling features.");

                            }
                        }
                    });
               }
         }
    });
}

This will work across reboots and without a network connection, provided the user purchased the item.



回答3:

Since you already know that it's impossible to make it unhackable using this system, I would recommend not attempting to prevent hacking. What you are proposing is known as "Security through obscurity" and is usually a bad idea.

My advice would be to try queryInventoryAsync() first, and only check your 'isPremium' flag if there is no internet connection.

There are also a few potential other ways of going about this, such as having separate free and premium apps, instead of an in app purchase. How other people handle this and the tools Google makes available might warrant an investigation.

queryInventoryAsync will automatically take into account uninstall and reinstalls, as it tracks purchases for the logged in user.