libgdx / gdx-pay

A libGDX cross-platform API for InApp purchasing.
Apache License 2.0
225 stars 83 forks source link

Return the original purchase response to the application #148

Closed lozan closed 5 years ago

lozan commented 7 years ago

Return the original purchase response to the application

In many circumstances it would be very beneficial to return the original purchase response data to the application. For iOS we can get that from Transaction.getTransactionDataSignature(), as Base64 encoded string. On Android the data is stripped out and only some fields are copied to a Transaction object.

In PurchaseResponseActivityResultConverter.java, in the function convertToTransaction(Intent) the data from the Intent is converted to a Transaction object, and that's where the original data is lost. In order to get the whole data, besides INAPP_PURCHASE_DATA, we would also need to get INAPP_DATA_SIGNATURE and keep them both somehow in the Transaction object, even if they have to be encoded in some way.

I believe the proposed improvement will be beneficial to many users who, like myself, do some kind of verification on custom server implementation. For example, on my custom server that's used for validating purchases I use the SKU, signature and the original JSON from the purchase, along with my Base64 encoded public key which is of course kept in the application itself. But now that I want to switch some of my apps to use gdx-pay, this one is a show stopper for me.

I hope it can be added as a feature in gdx-pay and hopefully other developers will benefit from it as well.

keesvandieren commented 7 years ago

This is a valuable add-on.

If you submit a Pull-Request, I'm happy to accept it and bring out a new release.

It is easy to test a change locally, instructions can be found here: https://github.com/libgdx/gdx-pay/#user-content-using-gdx-pay-locally-build-binaries-in-a-project

keesvandieren commented 7 years ago

The field Transaction.transactionDataSignature can be used for that? That one is also used by iOS.

noblemaster commented 7 years ago

The Transaction object and especially the transactionDataSignature already contains everything you need for server-side validation. I am using it already.

Source: https://github.com/libgdx/gdx-pay/blob/master/gdx-pay-server/src/com/badlogic/gdx/pay/server/impl/PurchaseVerifieriOSApple.java

The code above needs some tweaks. I have a slightly modified version of the above. I believe the getTransationData() in the code above should be the getTransationDataSignature() instead for it to work.

lozan commented 7 years ago

Yes, in the code above getTransationData() should be the getTransationDataSignature(). I am using the same approach for iOS purchase verification. But what I need is the original receipt for Android, not iOS.

noblemaster commented 7 years ago

Sorry, read it too quickly. All you need is there for Android as well, all you need is the server-side verification. Same as for iOS, use the translation.getTransationDataSignature() data for the verification.

I was going to add it to gdx-pay, but never found the time. Below is the code I use. The "Purchase" object needs to be replaced by gdx-pay's "Transaction" object. Then just get it to compile. If you don't mind, please get it to compile for gdx-pay & submit a pull-request!

import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.model.ProductPurchase;
import com.noblemaster.lib.boot.arch.control.log.Log;
import com.noblemaster.lib.boot.arch.control.Market;
import com.noblemaster.lib.boot.arch.service.Cashier;
import com.noblemaster.lib.boot.arch.service.Purchase;
import com.noblemaster.lib.core.data.Failure;
import com.noblemaster.lib.boot.plaf.impl.commondesk.tool.net.OAuth20;

/**
 * Server-side purchase verification for Google Play.
 * <p>
 * Required JARs:
 * <ul>
 *   <li>google-api-services-androidpublisher-v2-rev20141111-1.19.0.jar
 *   <li>google-api-client-1.19.0.jar
 *   <li>google-oauth-client-1.19.0.jar
 *   <li>google-http-client-1.19.0.jar
 *   <li>jsr305-1.3.9.jar
 *   <li>google-http-client-jackson2-1.19.0.jar (when using Jackson 2)
 *   <li>jackson-core-$2.1.3.jar
 * </ul>
 * Information:
 * </ul>
 *   <li>Our credentials are here: https://console.developers.google.com/apis/credentials
 *   <li>We use project "Google Play Android Developer" (stay the same for multiple projects)
 * </ul>
 * To obtain OAuth, refer to the following pages:
 * <ul>
 *   <li>https://developers.google.com/android-publisher/authorization
 *   <li>http://stackoverflow.com/questions/11115381/unable-to-get-the-subscription-information-from-google-play-android-developer-ap
 * </ul>
 *
 * @author noblemaster
 */
public class CashierVerifyGoogle implements Cashier.Verify {

  private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
  private static final JsonFactory JSON_FACTORY = new com.google.api.client.json.jackson2.JacksonFactory();

  /** The application name. Suggested format is "MyCompany-Application/1.0". */
  private String productName;
  /** The application package, e.g. "com.ageofconquest.app.user.aoc". */
  private String productPack;

  /** Client ID from Google Developer Console. */
  private String clientID;
  /** Client Secret from Google Developer Console. */
  private String clientSecret;
  /** The refresh-token: generate via our "OAuth20Printer.java"!. */
  private String refreshToken;

  public CashierVerifyGoogle(String productName, String productPack, String clientID, String clientSecret, String refreshToken) {
    this.productName = productName;
    this.productPack = productPack;

    // store connection-information
    this.clientID = clientID;
    this.clientSecret = clientSecret;
    this.refreshToken = refreshToken;
  }

  @Override
  public Market exchange() {
    return Market.GOOGLE;
  }

  @Override
  public void valid(Purchase purchase, String location) throws Failure {
    try {
      // obtain access-token (only temporary, so we need to refresh every time!)
      String accessToken = OAuth20.getAccessToken(OAuth20.PATH_GOOGLE, clientID, clientSecret, refreshToken);

      // purchase parameters
      String productId = purchase.getResourceExchange();
      String purchaseToken = purchase.getPurchaseDataSignature();

      // use AndroidPublisher to access our purchase-data!
      TokenResponse tokenResponse = new TokenResponse();
      tokenResponse.setAccessToken(accessToken);
      tokenResponse.setRefreshToken(refreshToken);
      tokenResponse.setExpiresInSeconds(3600L);
      tokenResponse.setScope("https://www.googleapis.com/auth/androidpublisher");
      tokenResponse.setTokenType("Bearer");

      HttpRequestInitializer credential =  new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
                                                                         .setJsonFactory(JSON_FACTORY)
                                                                         .setClientSecrets(clientID, clientSecret)
                                                                         .build()
                                                                         .setFromTokenResponse(tokenResponse);
      AndroidPublisher publisher = new AndroidPublisher.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
                                                       .setApplicationName(productName)
                                                       .build();

      // we use the publisher to obtain the purchase data (if valid, we should receive it!)
      AndroidPublisher.Purchases purchases = publisher.purchases();
      AndroidPublisher.Purchases.Products.Get purchasesGet = purchases.products().get(productPack, productId, purchaseToken);
      ProductPurchase productPurchase = purchasesGet.execute();

      // verify purchase
      if (productPurchase != null) {
        // log debug-info as needed!
        Log.d("GooglePlay purchase information successfully located/verified for purchase " + purchase + ".");
      }
      else {
        // error: not purchase found!
        throw new Failure("No purchase information located (i.e. 'null') originating from \"" + location + "\".");
      }
    }
    catch (Exception e) {
      // output & throw error
      boolean minimal = true;  // <-- mostly hackers: we just log the purchase info & IP and consider it done.
      if (minimal) {
        // no stacktrace
        Log.i("CashierVerifyGoogle: Error verifying purchase originating from \"" + location + "\" (" + purchase + ", purchaseText = \"" + purchase.getPurchaseText() + "\").");
      }
      else {
        // with stacktrace...
        Log.e("CashierVerifyGoogle: Error verifying purchase originating from \"" + location + "\" (" + purchase + ", purchaseText = \"" + purchase.getPurchaseText() + "\").", e);
      }
      throw Failure.e(e);
    }
  }
}
keesvandieren commented 7 years ago

@lozan , @noblemaster , Just updated the PurchaseResponseActivityResultConverter to also set Transaction transactionDataSignature property.

Can you please verify that of actual purchases, the dataSignature property is correctly filled?

You probably also want to have this in the getPurchases() method?

Reference here: https://developer.android.com/google/play/billing/billing_reference.html#getPurchases

getPurchases() returns INAPP_PURCHASE_DATA_LIST and a INAPP_DATA_SIGNATURE_LIST in its response data. Can those data be combined in one Transaction, by using the corresponding indices of the StringArrayLists of those?

lozan commented 7 years ago

The data signature seems to be filled in correctly.

You may check the IABHelper from googlesamples: https://github.com/googlesamples/android-play-billing/blob/master/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util/IabHelper.java

On line 535 and 536 they get the INAPP_PURCHASE_DATA and INAPP_DATA_SIGNATURE and then when they create their Purchase object on line 555, they keep that data as it is. The Purchase object is what's delivered to the application through the callback onIabPurchaseFinished.

In gdx-pay ideally those 2 fields would end up in the transactionData and transactionDataSignature of the Transaction object.

So, the transactionDataSignature is just fine with the new implementation. The transactionData doesn't seem to be ok, because in it there's the purchaseToken field from the INAPP_PURCHASE_DATA. In that field should be the whole INAPP_PURCHASE_DATA, and if the users want to extract the purchaseToken, they can do so. So convertJSONPurchaseToTransaction would be like this (note that the only thing that's changed here is the data that's passed in setTransactionData and the if that checks if there's a PURCHASE_TOKEN is taken out):

public static Transaction convertJSONPurchaseToTransaction(String inAppPurchaseData) throws JSONException {
    JSONObject object = new JSONObject(inAppPurchaseData);
    Transaction transaction = new Transaction();
    transaction.setStoreName(PurchaseManagerConfig.STORE_NAME_ANDROID_GOOGLE);
    transaction.setTransactionData(inAppPurchaseData);        
    if (object.has(ORDER_ID)) {
        transaction.setOrderId(object.getString(ORDER_ID));
    }
    transaction.setIdentifier(object.getString(PRODUCT_ID));
    transaction.setPurchaseTime(new Date(object.getLong(PURCHASE_TIME)));
    if (object.has(PURCHASE_STATE)) {
        fillTransactionForPurchaseState(transaction, object.getInt(PURCHASE_STATE));
    }
    return transaction;
}

To conclude, the latest changes that set the transactionDataSignature are just fine. If the converter is changed as proposed, i.e. to keep the value of INAPP_PURCHASE_DATA in the transactionData member, it should be all good.

Once it's changed, if there are existing users that are using the transactionData to actually get the purchaseToken, they'll have to change their implementation to actually extract the token from the JSON which will be in the transactionData. They also get the benefit that they may extract other fields from that data as well, which are described in "Table 6. Descriptions of the JSON fields for INAPP_PURCHASE_DATA: https://developer.android.com/google/play/billing/billing_reference.html

MrStahlfelge commented 5 years ago

@lozan How did you go forward with this issue? Are you using purchase verification in your app and can provide a PR on this?

gagbaghdas commented 5 years ago

@MrStahlfelge Hello. Any updates on this? I need the original receipt string too. Especially I need the developerPayload, cause all other fields I can get from the Transaction object and create the receipt manually(don't want to do it)).

MrStahlfelge commented 5 years ago

If there were updates, you would see it here. I am happy to accept any improvement PRs from you. If you don't want to do it, you can decide whether you wait for someone other to do it, to hire someone to do it or to skip the feature.

gagbaghdas commented 5 years ago

Hi @MrStahlfelge . Thanks for the response. I bypassed and have done my feature by another way as we needed it asap. P.S. I want to do it 👍 and will try to do once I'll have enough time

MrStahlfelge commented 5 years ago

We deprecated googleplay implementation in favor of the googlebilling implementation, so issues for the old implementation will be closed now. Please check if googlebilling works as intended for you.