Skip to content

Commit

Permalink
[in_app_purchase_android] Introduced new ReplacementMode for Android'…
Browse files Browse the repository at this point in the history
…s billing client (flutter#6515)

Introduced new `ReplacementMode` for Android's billing client and deprecated `ProrationMode`.

This PR is a follow-up on [https://github.com/flutter/packages/pull/6403](https://github.com/flutter/packages/pull/6403), where it was decided that we should not replace the `ProrationMode` with `ReplacementMode`, but instead only deprecate `ProrationMode`. The reason for a new PR is also that `in_app_purchase_android` version `0.3.3` changed internal platform communication to Pigeon, which meant I had to make major changes to my original PR.

*List which issues are fixed by this PR. You must list at least one issue.*
flutter#128957
  • Loading branch information
vongrad authored May 22, 2024
1 parent b1ffd1e commit 0bdda37
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 23 deletions.
4 changes: 4 additions & 0 deletions packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.6

* Introduces new `ReplacementMode` for Android's billing client as `ProrationMode` is being deprecated.

## 0.3.5+2

* Bumps androidx.annotation:annotation from 1.7.1 to 1.8.0.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v17.1.2), do not edit directly.
// Autogenerated from Pigeon (v17.3.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

package io.flutter.plugins.inapppurchase;
Expand Down Expand Up @@ -962,6 +962,19 @@ public void setProrationMode(@NonNull Long setterArg) {
this.prorationMode = setterArg;
}

private @NonNull Long replacementMode;

public @NonNull Long getReplacementMode() {
return replacementMode;
}

public void setReplacementMode(@NonNull Long setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"replacementMode\" is null.");
}
this.replacementMode = setterArg;
}

private @Nullable String offerToken;

public @Nullable String getOfferToken() {
Expand Down Expand Up @@ -1033,6 +1046,14 @@ public static final class Builder {
return this;
}

private @Nullable Long replacementMode;

@CanIgnoreReturnValue
public @NonNull Builder setReplacementMode(@NonNull Long setterArg) {
this.replacementMode = setterArg;
return this;
}

private @Nullable String offerToken;

@CanIgnoreReturnValue
Expand Down Expand Up @@ -1077,6 +1098,7 @@ public static final class Builder {
PlatformBillingFlowParams pigeonReturn = new PlatformBillingFlowParams();
pigeonReturn.setProduct(product);
pigeonReturn.setProrationMode(prorationMode);
pigeonReturn.setReplacementMode(replacementMode);
pigeonReturn.setOfferToken(offerToken);
pigeonReturn.setAccountId(accountId);
pigeonReturn.setObfuscatedProfileId(obfuscatedProfileId);
Expand All @@ -1088,9 +1110,10 @@ public static final class Builder {

@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(7);
ArrayList<Object> toListResult = new ArrayList<Object>(8);
toListResult.add(product);
toListResult.add(prorationMode);
toListResult.add(replacementMode);
toListResult.add(offerToken);
toListResult.add(accountId);
toListResult.add(obfuscatedProfileId);
Expand All @@ -1110,15 +1133,22 @@ ArrayList<Object> toList() {
: ((prorationMode instanceof Integer)
? (Integer) prorationMode
: (Long) prorationMode));
Object offerToken = list.get(2);
Object replacementMode = list.get(2);
pigeonResult.setReplacementMode(
(replacementMode == null)
? null
: ((replacementMode instanceof Integer)
? (Integer) replacementMode
: (Long) replacementMode));
Object offerToken = list.get(3);
pigeonResult.setOfferToken((String) offerToken);
Object accountId = list.get(3);
Object accountId = list.get(4);
pigeonResult.setAccountId((String) accountId);
Object obfuscatedProfileId = list.get(4);
Object obfuscatedProfileId = list.get(5);
pigeonResult.setObfuscatedProfileId((String) obfuscatedProfileId);
Object oldProduct = list.get(5);
Object oldProduct = list.get(6);
pigeonResult.setOldProduct((String) oldProduct);
Object purchaseToken = list.get(6);
Object purchaseToken = list.get(7);
pigeonResult.setPurchaseToken((String) purchaseToken);
return pigeonResult;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ class MethodCallHandlerImpl implements Application.ActivityLifecycleCallbacks, I
com.android.billingclient.api.BillingFlowParams.ProrationMode
.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;

@VisibleForTesting
static final int REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY =
com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
.UNKNOWN_REPLACEMENT_MODE;

private static final String TAG = "InAppPurchasePlugin";
private static final String LOAD_PRODUCT_DOC_URL =
"https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale";
Expand Down Expand Up @@ -285,9 +290,20 @@ public void queryProductDetailsAsync(
}
}

if (params.getProrationMode() != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
&& params.getReplacementMode()
!= REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
throw new FlutterError(
"IN_APP_PURCHASE_CONFLICT_PRORATION_MODE_REPLACEMENT_MODE",
"launchBillingFlow failed because you provided both prorationMode and replacementMode. You can only provide one of them.",
null);
}

if (params.getOldProduct() == null
&& params.getProrationMode()
!= PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
&& (params.getProrationMode()
!= PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
|| params.getReplacementMode()
!= REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY)) {
throw new FlutterError(
"IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT",
"launchBillingFlow failed because oldProduct is null. You must provide a valid oldProduct in order to use a proration mode.",
Expand Down Expand Up @@ -336,9 +352,16 @@ public void queryProductDetailsAsync(
&& !params.getOldProduct().isEmpty()
&& params.getPurchaseToken() != null) {
subscriptionUpdateParamsBuilder.setOldPurchaseToken(params.getPurchaseToken());
// Set the prorationMode using a helper to minimize impact of deprecation warning suppression.
setReplaceProrationMode(
subscriptionUpdateParamsBuilder, params.getProrationMode().intValue());
if (params.getProrationMode()
!= PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
setReplaceProrationMode(
subscriptionUpdateParamsBuilder, params.getProrationMode().intValue());
}
if (params.getReplacementMode()
!= REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
subscriptionUpdateParamsBuilder.setSubscriptionReplacementMode(
params.getReplacementMode().intValue());
}
paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build());
}
return fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build()));
Expand Down Expand Up @@ -385,7 +408,8 @@ public void queryPurchasesAsync(
}

try {
// Like in our connect call, consider the billing client responding a "success" here regardless
// Like in our connect call, consider the billing client responding a "success" here
// regardless
// of status code.
QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder();
paramsBuilder.setProductType(toProductTypeString(productType));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
Expand Down Expand Up @@ -556,6 +557,8 @@ public void launchBillingFlow_null_AccountId_do_not_crash() {
paramsBuilder.setProduct(productId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand All @@ -581,6 +584,8 @@ public void launchBillingFlow_ok_null_OldProduct() {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand Down Expand Up @@ -610,6 +615,8 @@ public void launchBillingFlow_ok_null_Activity() {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Assert that the synchronous call throws an exception.
FlutterError exception =
Expand All @@ -633,6 +640,8 @@ public void launchBillingFlow_ok_oldProduct() {
paramsBuilder.setOldProduct(oldProductId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand Down Expand Up @@ -660,6 +669,8 @@ public void launchBillingFlow_ok_AccountId() {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand Down Expand Up @@ -695,6 +706,8 @@ public void launchBillingFlow_ok_Proration() {
paramsBuilder.setOldProduct(oldProductId);
paramsBuilder.setPurchaseToken(purchaseToken);
paramsBuilder.setProrationMode((long) prorationMode);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand Down Expand Up @@ -728,6 +741,8 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setOldProduct(null);
paramsBuilder.setProrationMode((long) prorationMode);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand All @@ -744,6 +759,73 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() {
.contains("launchBillingFlow failed because oldProduct is null"));
}

@Test
@SuppressWarnings(value = "deprecation")
public void launchBillingFlow_ok_Replacement_with_null_OldProduct() {
// Fetch the product details first and query the method call
String productId = "foo";
String accountId = "account";
String queryOldProductId = "oldFoo";
int replacementMode =
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE;
queryForProducts(unmodifiableList(asList(productId, queryOldProductId)));
PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder();
paramsBuilder.setProduct(productId);
paramsBuilder.setAccountId(accountId);
paramsBuilder.setOldProduct(null);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode((long) replacementMode);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult);

// Assert that the synchronous call throws an exception.
FlutterError exception =
assertThrows(
FlutterError.class,
() -> methodChannelHandler.launchBillingFlow(paramsBuilder.build()));
assertEquals("IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", exception.code);
assertTrue(
Objects.requireNonNull(exception.getMessage())
.contains("launchBillingFlow failed because oldProduct is null"));
}

@Test
@SuppressWarnings(value = "deprecation")
public void launchBillingFlow_ok_Proration_and_Replacement_conflict() {
// Fetch the product details first and query the method call
String productId = "foo";
String accountId = "account";
String queryOldProductId = "oldFoo";
int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE;
int replacementMode =
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE;
queryForProducts(unmodifiableList(asList(productId, queryOldProductId)));
PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder();
paramsBuilder.setProduct(productId);
paramsBuilder.setAccountId(accountId);
paramsBuilder.setOldProduct(queryOldProductId);
paramsBuilder.setProrationMode((long) prorationMode);
paramsBuilder.setReplacementMode((long) replacementMode);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult);

// Assert that the synchronous call throws an exception.
FlutterError exception =
assertThrows(
FlutterError.class,
() -> methodChannelHandler.launchBillingFlow(paramsBuilder.build()));
assertEquals("IN_APP_PURCHASE_CONFLICT_PRORATION_MODE_REPLACEMENT_MODE", exception.code);
assertTrue(
Objects.requireNonNull(exception.getMessage())
.contains(
"launchBillingFlow failed because you provided both prorationMode and replacementMode. You can only provide one of them."));
}

// TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new
// ReplacementMode enum values.
// https://github.com/flutter/flutter/issues/128957.
Expand All @@ -763,6 +845,8 @@ public void launchBillingFlow_ok_Full() {
paramsBuilder.setOldProduct(oldProductId);
paramsBuilder.setPurchaseToken(purchaseToken);
paramsBuilder.setProrationMode((long) prorationMode);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Launch the billing flow
BillingResult billingResult = buildBillingResult();
Expand Down Expand Up @@ -790,6 +874,8 @@ public void launchBillingFlow_clientDisconnected() {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Assert that the synchronous call throws an exception.
FlutterError exception =
Expand All @@ -811,6 +897,8 @@ public void launchBillingFlow_productNotFound() {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Assert that the synchronous call throws an exception.
FlutterError exception =
Expand All @@ -835,6 +923,8 @@ public void launchBillingFlow_oldProductNotFound() {
paramsBuilder.setOldProduct(oldProductId);
paramsBuilder.setProrationMode(
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
paramsBuilder.setReplacementMode(
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);

// Assert that the synchronous call throws an exception.
FlutterError exception =
Expand Down
Loading

0 comments on commit 0bdda37

Please sign in to comment.