diff --git a/README.md b/README.md index 2f1d8a83..8ec7b6ed 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,25 @@ public final Date purchaseTime; public final PurchaseInfo purchaseInfo; ``` +## Getting Purchase History +You can request most recent purchases using `getPurchaseHistory` method. Pass required type as "inapp" for one-time purchases and "subs" for subscriptions +or use `Constants.PRODUCT_TYPE_MANAGED` and `Constants.PRODUCT_TYPE_SUBSCRIPTION` respectively. +```java +public List getPurchaseHistory(String type, Bundle extraParams) +``` +As a result you will get a `List` of `BillingHistoryRecord` objects with following fields: +```java +public final String productId; +public final String purchaseToken; +public final long purchaseTime; +public final String developerPayload; +public final String signature; +``` +Please keep in mind that this API requires `billing API` of version 6 or higher, so you should check if it is supported beforehand: +```java +public boolean isRequestBillingHistorySupported(String type) +``` + ## Handle Canceled Subscriptions Call `bp.getSubscriptionTransactionDetails(...)` and check the `purchaseInfo.purchaseData.autoRenewing` flag. diff --git a/library/src/androidTest/java/com/anjlab/android/iab/v3/BillingHistoryRecordTest.java b/library/src/androidTest/java/com/anjlab/android/iab/v3/BillingHistoryRecordTest.java new file mode 100644 index 00000000..450eec4f --- /dev/null +++ b/library/src/androidTest/java/com/anjlab/android/iab/v3/BillingHistoryRecordTest.java @@ -0,0 +1,51 @@ +package com.anjlab.android.iab.v3; + +import android.os.Parcel; + +import com.anjlab.android.iab.v3.util.ResourcesUtil; + +import org.json.JSONException; +import org.junit.Before; +import org.junit.Test; +import static junit.framework.Assert.assertEquals; + +public class BillingHistoryRecordTest +{ + + private String historyResponseJson; + + @Before + public void setup() + { + historyResponseJson = ResourcesUtil.loadFile("purchase_history_response.json"); + } + + @Test + public void testCreatesFromJsonCorrectly() throws JSONException + { + BillingHistoryRecord record = new BillingHistoryRecord(historyResponseJson, "signature"); + + assertEquals("sample-product-id", record.productId); + assertEquals("sample-purchase-token", record.purchaseToken); + assertEquals(1563441231403L, record.purchaseTime); + assertEquals("sample-developer-payload", record.developerPayload); + assertEquals("signature", record.signature); + } + + @Test + public void testParcelizesCorrectly() throws JSONException + { + BillingHistoryRecord record = new BillingHistoryRecord(historyResponseJson, "signature"); + + Parcel parcel = Parcel.obtain(); + record.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + BillingHistoryRecord restoredRecord = BillingHistoryRecord.CREATOR.createFromParcel(parcel); + assertEquals("sample-product-id", restoredRecord.productId); + assertEquals("sample-purchase-token", restoredRecord.purchaseToken); + assertEquals(1563441231403L, restoredRecord.purchaseTime); + assertEquals("sample-developer-payload", restoredRecord.developerPayload); + assertEquals("signature", restoredRecord.signature); + } +} diff --git a/library/src/androidTest/resources/purchase_history_response.json b/library/src/androidTest/resources/purchase_history_response.json new file mode 100644 index 00000000..7caf06d4 --- /dev/null +++ b/library/src/androidTest/resources/purchase_history_response.json @@ -0,0 +1,6 @@ +{ + "productId":"sample-product-id", + "purchaseToken":"sample-purchase-token", + "purchaseTime":1563441231403, + "developerPayload":"sample-developer-payload" +} \ No newline at end of file diff --git a/library/src/main/java/com/anjlab/android/iab/v3/BillingCommunicationException.java b/library/src/main/java/com/anjlab/android/iab/v3/BillingCommunicationException.java new file mode 100644 index 00000000..ea113646 --- /dev/null +++ b/library/src/main/java/com/anjlab/android/iab/v3/BillingCommunicationException.java @@ -0,0 +1,15 @@ +package com.anjlab.android.iab.v3; + +public class BillingCommunicationException extends Exception +{ + + public BillingCommunicationException(Throwable cause) + { + super(cause); + } + + public BillingCommunicationException(String message) + { + super(message); + } +} diff --git a/library/src/main/java/com/anjlab/android/iab/v3/BillingHistoryRecord.java b/library/src/main/java/com/anjlab/android/iab/v3/BillingHistoryRecord.java new file mode 100644 index 00000000..de1cb5b0 --- /dev/null +++ b/library/src/main/java/com/anjlab/android/iab/v3/BillingHistoryRecord.java @@ -0,0 +1,93 @@ +package com.anjlab.android.iab.v3; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.json.JSONException; +import org.json.JSONObject; + +public class BillingHistoryRecord implements Parcelable +{ + + public final String productId; + public final String purchaseToken; + public final long purchaseTime; + public final String developerPayload; + public final String signature; + + public BillingHistoryRecord(String dataAsJson, String signature) throws JSONException + { + this(new JSONObject(dataAsJson), signature); + } + + public BillingHistoryRecord(JSONObject json, String signature) throws JSONException + { + productId = json.getString("productId"); + purchaseToken = json.getString("purchaseToken"); + purchaseTime = json.getLong("purchaseTime"); + developerPayload = json.getString("developerPayload"); + this.signature = signature; + } + + public BillingHistoryRecord(String productId, String purchaseToken, long purchaseTime, + String developerPayload, String signature) + { + this.productId = productId; + this.purchaseToken = purchaseToken; + this.purchaseTime = purchaseTime; + this.developerPayload = developerPayload; + this.signature = signature; + } + + protected BillingHistoryRecord(Parcel in) + { + productId = in.readString(); + purchaseToken = in.readString(); + purchaseTime = in.readLong(); + developerPayload = in.readString(); + signature = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) + { + dest.writeString(productId); + dest.writeString(purchaseToken); + dest.writeLong(purchaseTime); + dest.writeString(developerPayload); + dest.writeString(signature); + } + + @Override + public int describeContents() + { + return 0; + } + + public static final Creator CREATOR = new Creator() + { + @Override + public BillingHistoryRecord createFromParcel(Parcel in) + { + return new BillingHistoryRecord(in); + } + + @Override + public BillingHistoryRecord[] newArray(int size) + { + return new BillingHistoryRecord[size]; + } + }; + + @Override + public String toString() + { + return "BillingHistoryRecord{" + + "productId='" + productId + '\'' + + ", purchaseToken='" + purchaseToken + '\'' + + ", purchaseTime=" + purchaseTime + + ", developerPayload='" + developerPayload + '\'' + + ", signature='" + signature + '\'' + + '}'; + } +} 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 c067e32d..789ac908 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,6 +34,7 @@ import com.android.vending.billing.IInAppBillingService; +import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -489,6 +490,42 @@ public boolean isOneTimePurchaseWithExtraParamsSupported(Bundle extraParams) return isOneTimePurchaseExtraParamsSupported; } + /** + * Checks if API supports version 6 which required to request purchase history + * @param type product type, accepts either {@value Constants#PRODUCT_TYPE_MANAGED} + * or {@value Constants#PRODUCT_TYPE_SUBSCRIPTION} + * @return {@code true} if feature supported {@code false} otherwise + */ + public boolean isRequestBillingHistorySupported(String type) throws BillingCommunicationException + { + if (!type.equals(Constants.PRODUCT_TYPE_MANAGED) && !type.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION)) + { + throw new RuntimeException("Unsupported type " + type); + } + + IInAppBillingService billing = billingService; + + if (billing != null) + { + + try + { + int response = billing.isBillingSupported(Constants.GOOGLE_API_REQUEST_PURCHASE_HISTORY_VERSION, + contextPackageName, type); + return response == Constants.BILLING_RESPONSE_RESULT_OK; + } + catch (RemoteException e) + { + throw new BillingCommunicationException(e); + } + + } + else + { + throw new BillingCommunicationException("Billing service isn't connected"); + } + } + /** * Change subscription i.e. upgrade or downgrade * @@ -1021,4 +1058,81 @@ private void reportBillingError(int errorCode, Throwable error) eventHandler.onBillingError(errorCode, error); } } + + /** + * Returns the most recent purchase made by the user for each SKU, even if that purchase is expired, canceled, or consumed. + * + * @param type product type, accepts either {@value Constants#PRODUCT_TYPE_MANAGED} or + * {@value Constants#PRODUCT_TYPE_SUBSCRIPTION} + * @param extraParams a Bundle with extra params that would be appended into http request + * query string. Not used at this moment. Reserved for future functionality. + * + * @return @NotNull list of billing history records + * @throws BillingCommunicationException if billing isn't connected or there was an error during request execution + */ + public List getPurchaseHistory(String type, Bundle extraParams) throws BillingCommunicationException + { + + if (!type.equals(Constants.PRODUCT_TYPE_MANAGED) && !type.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION)) + { + throw new RuntimeException("Unsupported type " + type); + } + + IInAppBillingService billing = billingService; + + if (billing != null) + { + + try + { + + List result = new ArrayList<>(); + int resultCode; + String continuationToken = null; + + do + { + + Bundle resultBundle = billing.getPurchaseHistory(Constants.GOOGLE_API_REQUEST_PURCHASE_HISTORY_VERSION, + contextPackageName, type, continuationToken, extraParams); + resultCode = resultBundle.getInt(Constants.RESPONSE_CODE); + + if (resultCode == Constants.BILLING_RESPONSE_RESULT_OK) + { + + List purchaseData = resultBundle.getStringArrayList(Constants.INAPP_PURCHASE_DATA_LIST); + + List signatures = resultBundle.getStringArrayList(Constants.INAPP_DATA_SIGNATURE_LIST); + + if (purchaseData != null && signatures != null) + { + + for (int i = 0, max = purchaseData.size(); i < max; i++) + { + String data = purchaseData.get(i); + String signature = signatures.get(i); + + BillingHistoryRecord record = new BillingHistoryRecord(data, signature); + result.add(record); + } + + continuationToken = resultBundle.getString(Constants.INAPP_CONTINUATION_TOKEN); + } + } + + } while (continuationToken != null && resultCode == Constants.BILLING_RESPONSE_RESULT_OK); + + return result; + + } catch (RemoteException | JSONException e) + { + throw new BillingCommunicationException(e); + } + + } + else + { + throw new BillingCommunicationException("Billing service isn't connected"); + } + } } 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 2c56f04c..a3980630 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 @@ -20,6 +20,7 @@ public class Constants public static final int GOOGLE_API_VERSION = 3; public static final int GOOGLE_API_SUBSCRIPTION_CHANGE_VERSION = 5; public static final int GOOGLE_API_VR_SUPPORTED_VERSION = 7; + public static final int GOOGLE_API_REQUEST_PURCHASE_HISTORY_VERSION = 6; public static final String PRODUCT_TYPE_MANAGED = "inapp"; public static final String PRODUCT_TYPE_SUBSCRIPTION = "subs"; @@ -59,6 +60,7 @@ public class Constants public static final String BUY_INTENT = "BUY_INTENT"; public static final String INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; public static final String INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; + public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; public static final String INAPP_DATA_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; public static final String RESPONSE_ORDER_ID = "orderId";