Skip to content

Commit

Permalink
Support for GetPurchaseHistory (anjlab#414)
Browse files Browse the repository at this point in the history
* New API for BillingProcessor:
isRequestBillingHistorySupported - to check if request purchase history supported
getPurchaseHistory - to request purchase history

* Updated code according to checkstyle

* Updated README to cover new API with getPurchaseHistory.
  • Loading branch information
syndarin authored and serggl committed Jul 23, 2019
1 parent 8d89370 commit 89de0cb
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<BillingHistoryRecord> 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"productId":"sample-product-id",
"purchaseToken":"sample-purchase-token",
"purchaseTime":1563441231403,
"developerPayload":"sample-developer-payload"
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<BillingHistoryRecord> CREATOR = new Creator<BillingHistoryRecord>()
{
@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 + '\'' +
'}';
}
}
114 changes: 114 additions & 0 deletions library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import com.android.vending.billing.IInAppBillingService;

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

import java.util.ArrayList;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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<BillingHistoryRecord> 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<BillingHistoryRecord> 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<String> purchaseData = resultBundle.getStringArrayList(Constants.INAPP_PURCHASE_DATA_LIST);

List<String> 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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down

0 comments on commit 89de0cb

Please sign in to comment.