Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address a bug with a developer payload check for the promo codes #295

Merged
merged 2 commits into from
Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,7 @@ Also notice, that you will need to call periodically `bp.loadOwnedPurchasesFromG

## Promo Codes Support

You can use promo codes along with this library, they are supported with one extra notice. According to [this issue](https://github.com/googlesamples/android-play-billing/issues/7)
there is currently a bug in Google Play Services, and it does not respect _Developer Payload_ token made by this library, causing a security validation fault (`BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD` error code).
While Google engineers are working on fixing this (lets hope so, you can also leave a feedback on this issue to make them work faster).

Still, there are couple of workarounds you can use:

1. Handle `BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD` error code in your `onBillingError` implementation. You can check out [#156](https://github.com/anjlab/android-inapp-billing-v3/issues/156) for a suggested workaround. This does not look nice, but it works.
2. Avoid using promo codes in a purchase dialog, prefer entering these codes in Google Play's App _Redeem promo code_ menu.
One way to do this is to distribute your promo codes in form of a redeem link (`https://play.google.com/redeem?code=YOURPROMOCODE`) instead of just a `YOURPROMOCODE` values.
You can find a sample on how to bundle it inside your app [here](https://gist.github.com/Thomas-Vos/6d44b4920dbdc8482a2467d95f66c5df).
You can use promo codes along with this library.

## Protection Against Fake "Markets"

Expand Down
4 changes: 4 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Upgrading Android In-App Billing v3 Library

### Upgrading to >= 1.0.44

The workaround below for the promo codes should no longer be valid. Promo codes should work just fine right out of the box

### Upgrading to >= 1.0.37

If you were supporting promo codes and faced troubled described in #156,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@

import com.android.vending.billing.IInAppBillingService;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
Expand Down Expand Up @@ -894,24 +893,20 @@ public TransactionDetails getSubscriptionTransactionDetails(String productId)
return getPurchaseTransactionDetails(productId, cachedSubscriptions);
}

private String getDeveloperPayloadFromPurchaseData(JSONObject purchase)
private String detectPurchaseTypeFromPurchaseResponseData(JSONObject purchase)
{
String value = null;
try
String purchasePayload = getPurchasePayload();
// regular flow, based on developer payload
if (!TextUtils.isEmpty(purchasePayload) && purchasePayload.startsWith(Constants.PRODUCT_TYPE_SUBSCRIPTION))
{
value = purchase.has(Constants.RESPONSE_PAYLOAD)
? purchase.getString(Constants.RESPONSE_PAYLOAD) : null;
return Constants.PRODUCT_TYPE_SUBSCRIPTION;
}
catch (JSONException e)
// backup check for the promo codes (no payload available)
if (purchase != null && purchase.has(Constants.RESPONSE_AUTO_RENEWING))
{
Log.e(LOG_TAG, "Failed to extract developer payload value!");
return Constants.PRODUCT_TYPE_SUBSCRIPTION;
}
return value != null ? value : "";
}

private boolean validateDeveloperPayload(String expectedValue, String actualValue)
{
return expectedValue.equals(actualValue);
return Constants.PRODUCT_TYPE_MANAGED;
}

public boolean handleActivityResult(int requestCode, int resultCode, Intent data)
Expand All @@ -927,54 +922,40 @@ public boolean handleActivityResult(int requestCode, int resultCode, Intent data
}
int responseCode = data.getIntExtra(Constants.RESPONSE_CODE, Constants.BILLING_RESPONSE_RESULT_OK);
Log.d(LOG_TAG, String.format("resultCode = %d, responseCode = %d", resultCode, responseCode));
String purchasePayload = getPurchasePayload();
if (resultCode == Activity.RESULT_OK &&
responseCode == Constants.BILLING_RESPONSE_RESULT_OK &&
!TextUtils.isEmpty(purchasePayload))
responseCode == Constants.BILLING_RESPONSE_RESULT_OK)
{
String purchaseData = data.getStringExtra(Constants.INAPP_PURCHASE_DATA);
String dataSignature = data.getStringExtra(Constants.RESPONSE_INAPP_SIGNATURE);
try
{
JSONObject purchase = new JSONObject(purchaseData);
String productId = purchase.getString(Constants.RESPONSE_PRODUCT_ID);
String developerPayload = getDeveloperPayloadFromPurchaseData(purchase);
boolean purchasedSubscription =
purchasePayload.startsWith(Constants.PRODUCT_TYPE_SUBSCRIPTION);
if (validateDeveloperPayload(purchasePayload, developerPayload))
if (verifyPurchaseSignature(productId, purchaseData, dataSignature))
{
if (verifyPurchaseSignature(productId, purchaseData, dataSignature))
{
BillingCache cache =
purchasedSubscription ? cachedSubscriptions : cachedProducts;
cache.put(productId, purchaseData, dataSignature);
if (eventHandler != null)
{
eventHandler.onProductPurchased(productId,
new TransactionDetails(new PurchaseInfo(
purchaseData,
dataSignature)));
}
}
else
String purchaseType = detectPurchaseTypeFromPurchaseResponseData(purchase);
BillingCache cache = purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION)
? cachedSubscriptions : cachedProducts;
cache.put(productId, purchaseData, dataSignature);
if (eventHandler != null)
{
Log.e(LOG_TAG, "Public key signature doesn't match!");
reportBillingError(Constants.BILLING_ERROR_INVALID_SIGNATURE, null);
eventHandler.onProductPurchased(
productId,
new TransactionDetails(new PurchaseInfo(purchaseData, dataSignature)));
}
}
else
{
Log.e(LOG_TAG, String.format("Payload mismatch: %s != %s",
purchasePayload,
developerPayload));
reportBillingError(Constants.BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD, null);
}
else
{
Log.e(LOG_TAG, "Public key signature doesn't match!");
reportBillingError(Constants.BILLING_ERROR_INVALID_SIGNATURE, null);
}
}
catch (Exception e)
{
Log.e(LOG_TAG, "Error in handleActivityResult", e);
reportBillingError(Constants.BILLING_ERROR_OTHER_ERROR, e);
}
savePurchasePayload(null);
}
else
{
Expand All @@ -983,8 +964,7 @@ public boolean handleActivityResult(int requestCode, int resultCode, Intent data
return true;
}

private boolean verifyPurchaseSignature(String productId, String purchaseData,
String dataSignature)
private boolean verifyPurchaseSignature(String productId, String purchaseData, String dataSignature)
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,19 @@ public class Constants
public static final String INAPP_DATA_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
public static final String RESPONSE_ORDER_ID = "orderId";
public static final String RESPONSE_PRODUCT_ID = "productId";
public static final String RESPONSE_PACKAGE_NAME = "packageName";
public static final String RESPONSE_PURCHASE_TIME = "purchaseTime";
public static final String RESPONSE_PURCHASE_STATE = "purchaseState";
public static final String RESPONSE_PURCHASE_TOKEN = "purchaseToken";
public static final String RESPONSE_DEVELOPER_PAYLOAD = "developerPayload";
public static final String RESPONSE_TYPE = "type";
public static final String RESPONSE_TITLE = "title";
public static final String RESPONSE_DESCRIPTION = "description";
public static final String RESPONSE_PRICE = "price";
public static final String RESPONSE_PRICE_CURRENCY = "price_currency_code";
public static final String RESPONSE_PRICE_MICROS = "price_amount_micros";
public static final String RESPONSE_PAYLOAD = "developerPayload";
public static final String RESPONSE_SUBSCRIPTION_PERIOD = "subscriptionPeriod";
public static final String RESPONSE_AUTO_RENEWING = "autoRenewing";
public static final String RESPONSE_FREE_TRIAL_PERIOD = "freeTrialPeriod";
public static final String RESPONSE_INTRODUCTORY_PRICE = "introductoryPrice";
public static final String RESPONSE_INTRODUCTORY_PRICE_MICROS = "introductoryPriceAmountMicros";
Expand All @@ -82,6 +87,7 @@ public class Constants
public static final int BILLING_ERROR_INVALID_SIGNATURE = 102;
public static final int BILLING_ERROR_LOST_CONTEXT = 103;
public static final int BILLING_ERROR_INVALID_MERCHANT_ID = 104;
@Deprecated
public static final int BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD = 105;
public static final int BILLING_ERROR_OTHER_ERROR = 110;
public static final int BILLING_ERROR_CONSUME_FAILED = 111;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ PurchaseData parseResponseDataImpl()
{
JSONObject json = new JSONObject(responseData);
PurchaseData data = new PurchaseData();
data.orderId = json.optString("orderId");
data.packageName = json.optString("packageName");
data.productId = json.optString("productId");
long purchaseTimeMillis = json.optLong("purchaseTime", 0);
data.orderId = json.optString(Constants.RESPONSE_ORDER_ID);
data.packageName = json.optString(Constants.RESPONSE_PACKAGE_NAME);
data.productId = json.optString(Constants.RESPONSE_PRODUCT_ID);
long purchaseTimeMillis = json.optLong(Constants.RESPONSE_PURCHASE_TIME, 0);
data.purchaseTime = purchaseTimeMillis != 0 ? new Date(purchaseTimeMillis) : null;
data.purchaseState = PurchaseState.values()[json.optInt("purchaseState", 1)];
data.developerPayload = json.optString("developerPayload");
data.purchaseToken = json.getString("purchaseToken");
data.autoRenewing = json.optBoolean("autoRenewing");
data.purchaseState = PurchaseState.values()[json.optInt(Constants.RESPONSE_PURCHASE_STATE, 1)];
data.developerPayload = json.optString(Constants.RESPONSE_DEVELOPER_PAYLOAD);
data.purchaseToken = json.getString(Constants.RESPONSE_PURCHASE_TOKEN);
data.autoRenewing = json.optBoolean(Constants.RESPONSE_AUTO_RENEWING);
return data;
}
catch (JSONException e)
Expand Down