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

Support for GetPurchaseHistory #414

Merged
merged 3 commits into from
Jul 23, 2019
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need a local var here?

Copy link
Contributor Author

@syndarin syndarin Jul 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this.billingService becomes null if service disconnects. So it is possible to get NPE if method called in secondary thread.


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