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

[in_app_purchase_android] Introduced new ReplacementMode for Android's billing client #6515

Merged
merged 9 commits into from
May 22, 2024
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

* Replaces `getCountryCode` with `countryCode`.
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:/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:/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