diff --git a/README.md b/README.md index 55f70d84..c32216f0 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/UPGRADING.md b/UPGRADING.md index ca25b10a..98427395 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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, diff --git a/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java b/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java index 2d531710..046d5159 100644 --- a/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java +++ b/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java @@ -34,7 +34,6 @@ import com.android.vending.billing.IInAppBillingService; -import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -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) @@ -927,10 +922,8 @@ 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); @@ -938,43 +931,31 @@ public boolean handleActivityResult(int requestCode, int resultCode, Intent data { 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 { @@ -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 { diff --git a/library/src/main/java/com/anjlab/android/iab/v3/Constants.java b/library/src/main/java/com/anjlab/android/iab/v3/Constants.java index 3900a932..2c56f04c 100644 --- a/library/src/main/java/com/anjlab/android/iab/v3/Constants.java +++ b/library/src/main/java/com/anjlab/android/iab/v3/Constants.java @@ -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"; @@ -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; diff --git a/library/src/main/java/com/anjlab/android/iab/v3/PurchaseInfo.java b/library/src/main/java/com/anjlab/android/iab/v3/PurchaseInfo.java index 3e4fc728..4dd0e434 100644 --- a/library/src/main/java/com/anjlab/android/iab/v3/PurchaseInfo.java +++ b/library/src/main/java/com/anjlab/android/iab/v3/PurchaseInfo.java @@ -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)