Skip to content

Commit

Permalink
fix: Avoid consuming all purchase blindly, but only pending ones
Browse files Browse the repository at this point in the history
If the purchase is really pending, nothing will happen (error). Otherwise, the Play Store cache will
be force updated
  • Loading branch information
amauryliet committed Aug 26, 2020
1 parent 4df14d7 commit 7d20b56
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 21 deletions.
85 changes: 64 additions & 21 deletions android/src/main/java/com/dooboolab/RNIap/RNIapModule.java
Expand Up @@ -20,6 +20,7 @@

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

Expand Down Expand Up @@ -177,6 +178,35 @@ public void endConnection(final Promise promise) {
}
}

private void consumeItems(final List<Purchase> purchases, final Promise promise) {
consumeItems(purchases, promise, BillingClient.BillingResponseCode.OK);
}

private void consumeItems(final List<Purchase> purchases, final Promise promise, final int expectedResponseCode) {
for (Purchase purchase : purchases) {
final ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();

final ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String outToken) {
if (billingResult.getResponseCode() != expectedResponseCode) {
DoobooUtils.getInstance().rejectPromiseWithBillingError(promise, billingResult.getResponseCode());
return;
}
try {
promise.resolve(true);
} catch (ObjectAlreadyConsumedException oce) {
promise.reject(oce.getMessage());
}
}
};
billingClient.consumeAsync(consumeParams, listener);
}
}

@ReactMethod
public void refreshItems(final Promise promise) {
// Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
Expand All @@ -196,29 +226,42 @@ public void run() {
return;
}

consumeItems(purchases, promise);
}
});
}

@ReactMethod
public void flushFailedPurchasesCachedAsPending(final Promise promise) {
ensureConnection(promise, new Runnable() {
@Override
public void run() {
final WritableNativeArray array = new WritableNativeArray();
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
if (result == null) {
// No results for query
promise.resolve(false);
return;
}
final List<Purchase> purchases = result.getPurchasesList();
if (purchases == null) {
// No purchases found
promise.resolve(false);
return;
}
final List<Purchase> pendingPurchases = Collections.EMPTY_LIST;
for (Purchase purchase : purchases) {
final ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();

final ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String outToken) {
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
DoobooUtils.getInstance().rejectPromiseWithBillingError(promise, billingResult.getResponseCode());
return;
}
array.pushString(outToken);
try {
promise.resolve(true);
} catch (ObjectAlreadyConsumedException oce) {
promise.reject(oce.getMessage());
}
}
};
billingClient.consumeAsync(consumeParams, listener);
// we only want to try to consume PENDING items, in order to force cache-refresh for them
if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
pendingPurchases.add(purchase);
}
}
if (pendingPurchases.size() == 0) {
promise.resolve(false);
return;
}

consumeItems(pendingPurchases, promise, BillingClient.BillingResponseCode.ITEM_NOT_OWNED);
}
});
}
Expand Down
18 changes: 18 additions & 0 deletions index.ts
Expand Up @@ -202,6 +202,10 @@ export const endConnectionAndroid = (): Promise<void> => {

/**
* Consume all remaining tokens. Android only.
* This is considered dangerous as you should deliver the purchased feature BEFORE consuming it.
* If you used this method to refresh Play Store cache (of failed pending payment still marked as failed),
* prefer using flushFailedPurchasesCachedAsPendingAndroid
* @deprecated
* @returns {Promise<string[]>}
*/
export const consumeAllItemsAndroid = (): Promise<string[]> =>
Expand All @@ -213,6 +217,19 @@ export const consumeAllItemsAndroid = (): Promise<string[]> =>
},
})();

/**
* Consume all 'ghost' purchases (that is, pending payment that already failed but is still marked as pending in Play Store cache). Android only.
* @returns {Promise<boolean>}
*/
export const flushFailedPurchasesCachedAsPendingAndroid = (): Promise<string[]> =>
Platform.select({
ios: async () => Promise.resolve(),
android: async () => {
await checkNativeAndroidAvailable();
return RNIapModule.flushFailedPurchasesCachedAsPending();
},
})();

/**
* Get a list of products (consumable and non-consumable items, but not subscriptions)
* @param {string[]} skus The item skus
Expand Down Expand Up @@ -729,6 +746,7 @@ const iapUtils = {
getAvailablePurchases,
getPendingPurchasesIOS,
consumeAllItemsAndroid,
flushFailedPurchasesCachedAsPendingAndroid,
clearProductsIOS,
clearTransactionIOS,
acknowledgePurchaseAndroid,
Expand Down

0 comments on commit 7d20b56

Please sign in to comment.