Billing V5: change between 2 renewable base plans of the same subscription
I am migrating the Android Billing Library from V4 to V5 and I cannot use the method BillingClient#launchBillingFlow to change between 2 renewable base plans of the same subscription. It returns the error Error retrieving information from server. DF-DFERH-01. Below is my implementation:
BillingFlowParams.SubscriptionUpdateParams subscriptionUpdateParams =
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(oldPurchaseToken)
.build();
List<BillingFlowParams.ProductDetailsParams> productDetailsParams = new ArrayList<>();
productDetailsParams.add(BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build());
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSubscriptionUpdateParams(subscriptionUpdateParams)
.setProductDetailsParamsList(productDetailsParams)
.build();
mBillingClient.launchBillingFlow(activity, billingFlowParams);
The official introduction about the new Billing structure mentioned the ability to change between 2 renewable base plans of the same subscription in their example:
In the PlayBillingCodelab sample app, when a user purchased a renewable plan, there is no option to upgrade/downgrade to another renewable plan in the same subscription. (PlayBillingCodelab MainActivity.kt line 127)
So which one is correct? In the new subscription structure, can the user change between 2 renewable base plans of the same subscription?

It is supported. It could be the fact that you didn't set a proration mode or something is wrong with the tokens, the old token must be the purchase token of the existing subscription while the new token is the offer token. See https://github.com/android/play-billing-samples/blob/3f34105c34e2ed2e88055ccf2b04088e8a5fd3a2/ClassyTaxiAppKotlin/app/src/main/java/com/example/subscriptions/ui/BillingViewModel.kt#L145
I can use the above block of code to change between 2 separate subscriptions, I understand the old purchase token and the offer token. If we do not set ProrationMode, the default one IMMEDIATE_WITH_TIME_PRORATION will be used (see SubscriptionUpdateParams.Builder#setReplaceProrationMode)
In the ClassyTaxiAppKotlin sample app, their only support the user to change from prepaid to renewable in the same subscription or change between 2 separate subscriptions, my issue is about to change between 2 renewable base plans of the same subscription, it is the key.
It works between different base plans on the same subscription, try it with the proration and check your tokens, I have it setup where I have a monthly and yearly base plan and I can switch between them.
@matale Could you please share your code and the purchase dialog UI? I checked the old purchase token and the offer token very carefully and set the ProrationMode but it still does not work. I wasted my whole week investigating the ability to change between 2 renewable base plans in the same subscription 🧐 Btw, do you have any idea why the sample app does not support it?
@matale Could you please share your code and the purchase dialog UI? I checked the old purchase token and the offer token very carefully and set the ProrationMode but it still does not work. I wasted my whole week investigating the ability to change between 2 renewable base plans in the same subscription 🧐 Btw, do you have any idea why the sample app does not support it?
I think the sample app is just intended to be simple example, sharing the code is a little complicated because its company code. Can you share yours in a repo or something and I will try and take a look.
I have the same pb i double checked old purchase token and still got DF-DFERH-01 when trying to upgrade/downgrade. A normal subscription works fine.
@tuan25abc @SamYStudiO I faced the same issue. It is only possible to change base plan of the same subscription with the ProrationMode.IMMEDIATE_WITHOUT_PRORATION and ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
Can I can between one offer to another offer as part of upgrade? With the second offer with eligibility criteria as "Upgrade" and including a 2 week trial period? Also, What purpose does the "Upgrade" here severs?
@tuan25abc @SamYStudiO I faced the same issue. It is only possible to change base plan of the same subscription with the
ProrationMode.IMMEDIATE_WITHOUT_PRORATIONandProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
I can confirm this. Although it seems like really strange decision by Google. Not sure if this is intended or a bug. Is there any update?
👋 hey all, i was having the same issue a while back but just re-tried and it seems like IMMEDIATE_WITH_TIME_PRORATION now works when switching between base plans. perhaps google changed something on their side? would love if anyone could confirm it's working for them too, i'm not sure i trust it 🤔
@RetRo99 i found this section for info on upgrading from one offer to another, hope that helps.
Version 5, The error is caused because the OldPurchaseToken is invalid or the subscription has expired.
You need to check if the user has an active subscription and return the Purchase to get the OldPurchaseToken using the .queryPurchasesAsync(....) from the billingClient
Here's an example to restore the purchase.
void restorePurchases() {
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases()
.setListener((billingResult, list) -> {}).build();
final BillingClient finalBillingClient = billingClient;
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingServiceDisconnected() {
}
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
finalBillingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build(), (billingResult1, list) -> {
if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (list.size() > 0) {
Log.d("Test1234",list.get(0).getPurchaseToken()); // This is the OldPurchaseToken
prefs.setPremium(1); // set 1 to activate premium feature
showSnackBar(btn_restore_fab, "Successfully restored");
} else {
showSnackBar(btn_restore_fab, "Oops, No purchase found.");
prefs.setPremium(0); // set 0 to de-activate premium feature
}
}
});
}
}
});
}
This is the upgrade method, with a valid OldPurchaseToken
void upgrade(){
ProductDetails productDetailsz = productDetails.get(0);
String offerToken = productDetailsz
.getSubscriptionOfferDetails().get(0)
.getOfferToken();
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
ImmutableList.of(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetailsz)
.setOfferToken(offerToken)
.build()))
.setSubscriptionUpdateParams(
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken("ENTER_THE_VALID_OLDPURCHASETOKEN_HERE")
.setReplaceProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE)
.build())
.build();
billingClient.launchBillingFlow(activity, billingFlowParams);
}