React-native-iap: Error when upgrade/downgrade subscription on Android with DEFERRED mode

Created on 28 Dec 2019  ·  31Comments  ·  Source: dooboolab/react-native-iap

Version of react-native-iap

4.3.3

Version of react-native

0.59.10

Platforms you faced the error (IOS or Android or both?)

Android

Expected behavior

When upgrade/downgrade a subscription with DEFERRED mode, purchase update listener should be called with new transaction receipt.

Actual behavior

purchase error listener is called with following error:

{ message: 'purchases are null.',
code: 'OK',
debugMessage: '',
responseCode: 0 }

Tested environment (Emulator? Real Device?)

Real Device - sandbox

Steps to reproduce the behavior

1.Purchase a subscription with RNIap.requestSubscription(sku), this works correctly.

2.Upgrade or downgrade the subscription with RNIap.requestSubscription(newSku, false, sku, 4). (4 is the DEFERRED Proration Mode) Google Play billing dialog will show the subscription plan is changed successfully, and you will receive a notification email from Google Play about that. However, the error listener is called with the above mentioned error.

3.If you call RNIap.requestSubscription(newSku, false, sku, 4) again, Google Play billing dialog will say they cannot change the subscription plan, and the error listener is called with the following error:

{ message: 'Google is indicating that we have some issue connecting to payment.',
code: 'E_DEVELOPER_ERROR',
debugMessage: '',
responseCode: 5 }

But sometimes Google Play billing dialog will say your order is under processing and your product should be delivered soon, and the error listener is called with the following error:

{ message: 'You already own this item.',
code: 'E_ALREADY_OWNED',
debugMessage: '',
responseCode: 7 }

I guess this is due to the transaction of step 2 is waiting to be acknowledged. However, since step 2 does not return a receipt, we cannot acknowledge it.

🕵️‍♂️ need more investigation 🙏 help wanted 🤖 android

Most helpful comment

Any plans to resolve this issue?

All 31 comments

Try to call getAvailablePurchases after you've upgrade/downgrade subscription.
Refer to doc, The list of Purchase objects in onPurchasesUpdated() does not contain paused subscriptions..

I am not quite sure tough hope it helps. Please feel free to leave any other trials and discussions.

I tried getAvailablePurchases() after upgrade/downgrade the subscription, but unfortunately, only the transaction receipt of original purchase is in the returned array.

RNIap v2.3.19 & v2.5.5 works correctly, buySubscription() will return a new receipt when upgrade/downgrade the subscription. But I would like to upgrade to v3 or v4 which has a better event based model if possible.

Here is a reference document which explains how Android subscription upgrade/downgrade works and why we need the new receipt after upgrade/downgrade.

@howg0924 Thanks for confirming this. I'd like to compare the differences with v2 and our current version and see how I can fix this issue.

I've looked up the code on the weekend for you in #893
The new update will be comming in 4.4.0.

Could you kindly test [email protected] version out?

@hyochan I just tried 4.4.0-rc.1, but the situation is still the same.

After calling RNIap.requestSubscription(oldSku, false, newSku 4) and finish the upgrade/downgrade, error listener is called with following error:

{ message: 'purchases are null.',
code: 'OK',
debugMessage: '',
responseCode: 0 }

And getAvailablePurchases() only contains old receipt.

@howg0924 If you know how to debug android, I hope you to test out putting some Log.d and see if it passes if conditions in buyItemByType. Would that possible for you? I want to see how they are being executed.

@howg0924 Oh wait~! I think I found the problem let me get back to you soon!

@howg0924 Could you try 4.4.0-rc.2? I think it will work this time.

@hyochan 4.4.0-rc.2 build failed on:

RNIapModule.java:436: error: incompatible types: SkuDetails cannot be converted to String:
   builder.setOldSku(selectedOldSku);
                     ^

@hyochan I monitored buyItemByType() of 4.4.0-rc.1. When upgrade/downgrade, it did execute:

builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.DEFERRED);

and also executed:

BillingResult billingResult = billingClient.launchBillingFlow(activity, flowParams);

Do you want me to monitor/dump any flow/data?

(removed due to some misunderstanding)

@howg0924 Sorry that I've made mistake because I did not have a good debugging env. I've reverted this in 4.4.0-rc.3. It looks like your code is not running builder.setOldSku(oldSku) which is most important.

After compare with 2.5.5, I found 2.5.5 does not call

builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.DEFERRED);

when using the DEFERRED mode.

So I try to remove this line on 4.4.0-rc.1, and it works! But, I don't know:

1.what proration mode it is working on now.
2.why it doesn't work when set to DEFERRED mode by setReplaceSkusProrationMode()

        if (prorationMode != null && prorationMode != -1) {
          if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE);
            if (type.equals(BillingClient.SkuType.SUBS) == false) {
              String debugMessage = "IMMEDIATE_AND_CHARGE_PRORATED_PRICE for proration mode only works in subscription purchase.";
              WritableMap error = Arguments.createMap();
              error.putString("debugMessage", debugMessage);
              error.putString("code", PROMISE_BUY_ITEM);
              error.putString("message", debugMessage);
              sendEvent(reactContext, "purchase-error", error);
              promise.reject(PROMISE_BUY_ITEM, debugMessage);
              return;
            }
          } else if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION);
          } else if (prorationMode == BillingFlowParams.ProrationMode.DEFERRED) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.DEFERRED);
          } else if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION);
          } else {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
          }
        }

We are currently passing down the ProrationMode as above. So does that mean you can auto renew subscription when you've removed DEFERRED?

I mean if I change code to:

        if (prorationMode != null && prorationMode != -1) {
          if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE);
            if (type.equals(BillingClient.SkuType.SUBS) == false) {
              String debugMessage = "IMMEDIATE_AND_CHARGE_PRORATED_PRICE for proration mode only works in subscription purchase.";
              WritableMap error = Arguments.createMap();
              error.putString("debugMessage", debugMessage);
              error.putString("code", PROMISE_BUY_ITEM);
              error.putString("message", debugMessage);
              sendEvent(reactContext, "purchase-error", error);
              promise.reject(PROMISE_BUY_ITEM, debugMessage);
              return;
            }
          } else if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION);
          } else if (prorationMode == BillingFlowParams.ProrationMode.DEFERRED) {
            // comment following line
            // builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.DEFERRED);
          } else if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION) {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION);
          } else {
            builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
          }
        }

then I can use RNIap.requestSubscription(newSku, false, sku, 4 /* DEFERRED mode */) to upgrade/downgrade a subscription. Though I am not sure what proration mode it actually works on since builder.setReplaceSkusProrationMode() is not executed, as v2.5.5 did.

Following code is from v2.5.5:

        if (type.equals(BillingClient.SkuType.SUBS) && oldSku != null && !oldSku.isEmpty()) {
          // Subscription upgrade/downgrade
          if (prorationMode != null && prorationMode != 0) {
            builder.setOldSku(oldSku);
            if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE) {
              builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE);
            } else if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION) {
              builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION);
            } else {
              builder.addOldSku(oldSku);
            }
          } else {
            builder.addOldSku(oldSku);
          }
        }

@howg0924 Well this is very strange since we did not even set DEFFERED in our current master branch which is 4.3.4. I've added this code in 4.4.0+.

I think the reason might be something different. I hope you can come back if there is some other news.

Related #391 #555

OK, I will try more and report back.

Related #707

@hyochan After further test on 4.4.0-rc.1:

IMMEDIATE_WITH_TIME_PRORATION => works
IMMEDIATE_AND_CHARGE_PRORATED_PRICE => works
IMMEDIATE_WITHOUT_PRORATION => works
DEFERRED => NOT WORK (don't know why)

Well this is very strange since we did not even set DEFFERED in our current master branch which is 4.3.4. I've added this code in 4.4.0+.

I believe this is because of the following code in 4.3.0 still set it to DEFERRED mode:

  if (prorationMode != 0 && prorationMode != -1) {
    builder.setReplaceSkusProrationMode(prorationMode);
  }

However, above code does not exist on 2.3.19 & 2.5.5. Therefore, when I called RNIap.requestSubscription(newSku, false, sku, 4 /* DEFERRED */) on 2.3.19 & 2.5.5, I thought it worked before, but now I realized it actually worked on the default IMMEDIATE_WITH_TIME_PRORATION mode.

Ok so we should know why the Deffered won't work. I couldn't find the reason for now.

I've got the same issue but behavior is different.

When I call requestSubscription(newSku, false, oldSku, 4), it throws an error with error code OK and empty error message. Payment on Google Play side is as expected (it's deferred). When the next subscription charge happens, I get purchaseUpdatedListener event on app start but unable to acknowledge the purchase (through calling finishTransaction()). finishTransaction() gives an DEVELOPER_ERROR error.

Happens on both version 4.4.0 and 4.3.4.

Hi @hyochan, I saw that in https://github.com/dooboolab/react-native-iap/pull/893 you marked this as resolved but I updated react-native-iap to latest version but this one is still not be fixed. I still got error in listener error {"code": "OK", "debugMessage": "", "message": "purchases are null.", "responseCode": 0} and received the email You updated your subscription purchase normally. So my question is what is the correct solution for DEFERRED mode?

Last time I've found this issue either. It strangely doesn't work only in deferred mode. We need to investigate this.

Hi @howg0924, did you get a solution for this issue yet?

@nenjamin2405 No, I am still using the default IMMEDIATE_WITH_TIME_PRORATION mode, and I really hope this could be fixed.

Hi @hyochan, any update on this? My app's new feature is stuck by this issue. Really appreciate your effort in investigating this, thanks

Not yet. I've not had time to go over this issue.
I hope somebody also brings up useful information to handle this issue.
Asking Google or StackOverflow and share that in this thread might be really helpful.

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

We've been looking at this issue and wanted to share with the community, it may help some people out there.

The issue is due to the purchase updated listener being called with a empty list of purchases on a deferred subscription replace, which is how Android works (See here here "For the deferred replacement mode...")

We added the support of android deferred subscription replace on react-native-iaphub by doing a fix looking when the error listener is called with the purchases are null error on a subscription replace. (See commit)

Of course you'll have to do some modifications on your server side validating your receipts as well, refreshing the receipt isn't enough when dealing with deferred subscriptions.
You'll have to implement the Android realtime notifications in order to detect when the subscription replace is occurring and process a new token.

Or you can just use IAPHUB which is doing all of that for you 🙂

Any plans to resolve this issue?

As @iaphub mentions, the issue is that onPurchasesUpdated is called with the purchases argument as null when DEFERRED mode is used.

From google docs:

For the deferred replacement mode, your app receives a call to your PurchasesUpdatedListener with an empty list of purchases and a status of whether the upgrade or downgrade was successful.

So it's just a matter of handling that case inside that listener.

But I'm uncertain about what should be the proper way of handling that situation:

  • Resolving the promise with the previous subscription info
  • Resolving the promise with undefined
  • Rejecting the promise with a PURCHASE_DEFERRED code

What do you think?

Was this page helpful?
0 / 5 - 0 ratings