Skip to content

Commit 0bdda37

Browse files
authored
[in_app_purchase_android] Introduced new ReplacementMode for Android's billing client (#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/flutter#128957
1 parent b1ffd1e commit 0bdda37

File tree

11 files changed

+269
-23
lines changed

11 files changed

+269
-23
lines changed

packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.6
2+
3+
* Introduces new `ReplacementMode` for Android's billing client as `ProrationMode` is being deprecated.
4+
15
## 0.3.5+2
26

37
* Bumps androidx.annotation:annotation from 1.7.1 to 1.8.0.

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// Autogenerated from Pigeon (v17.1.2), do not edit directly.
4+
// Autogenerated from Pigeon (v17.3.0), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
66

77
package io.flutter.plugins.inapppurchase;
@@ -962,6 +962,19 @@ public void setProrationMode(@NonNull Long setterArg) {
962962
this.prorationMode = setterArg;
963963
}
964964

965+
private @NonNull Long replacementMode;
966+
967+
public @NonNull Long getReplacementMode() {
968+
return replacementMode;
969+
}
970+
971+
public void setReplacementMode(@NonNull Long setterArg) {
972+
if (setterArg == null) {
973+
throw new IllegalStateException("Nonnull field \"replacementMode\" is null.");
974+
}
975+
this.replacementMode = setterArg;
976+
}
977+
965978
private @Nullable String offerToken;
966979

967980
public @Nullable String getOfferToken() {
@@ -1033,6 +1046,14 @@ public static final class Builder {
10331046
return this;
10341047
}
10351048

1049+
private @Nullable Long replacementMode;
1050+
1051+
@CanIgnoreReturnValue
1052+
public @NonNull Builder setReplacementMode(@NonNull Long setterArg) {
1053+
this.replacementMode = setterArg;
1054+
return this;
1055+
}
1056+
10361057
private @Nullable String offerToken;
10371058

10381059
@CanIgnoreReturnValue
@@ -1077,6 +1098,7 @@ public static final class Builder {
10771098
PlatformBillingFlowParams pigeonReturn = new PlatformBillingFlowParams();
10781099
pigeonReturn.setProduct(product);
10791100
pigeonReturn.setProrationMode(prorationMode);
1101+
pigeonReturn.setReplacementMode(replacementMode);
10801102
pigeonReturn.setOfferToken(offerToken);
10811103
pigeonReturn.setAccountId(accountId);
10821104
pigeonReturn.setObfuscatedProfileId(obfuscatedProfileId);
@@ -1088,9 +1110,10 @@ public static final class Builder {
10881110

10891111
@NonNull
10901112
ArrayList<Object> toList() {
1091-
ArrayList<Object> toListResult = new ArrayList<Object>(7);
1113+
ArrayList<Object> toListResult = new ArrayList<Object>(8);
10921114
toListResult.add(product);
10931115
toListResult.add(prorationMode);
1116+
toListResult.add(replacementMode);
10941117
toListResult.add(offerToken);
10951118
toListResult.add(accountId);
10961119
toListResult.add(obfuscatedProfileId);
@@ -1110,15 +1133,22 @@ ArrayList<Object> toList() {
11101133
: ((prorationMode instanceof Integer)
11111134
? (Integer) prorationMode
11121135
: (Long) prorationMode));
1113-
Object offerToken = list.get(2);
1136+
Object replacementMode = list.get(2);
1137+
pigeonResult.setReplacementMode(
1138+
(replacementMode == null)
1139+
? null
1140+
: ((replacementMode instanceof Integer)
1141+
? (Integer) replacementMode
1142+
: (Long) replacementMode));
1143+
Object offerToken = list.get(3);
11141144
pigeonResult.setOfferToken((String) offerToken);
1115-
Object accountId = list.get(3);
1145+
Object accountId = list.get(4);
11161146
pigeonResult.setAccountId((String) accountId);
1117-
Object obfuscatedProfileId = list.get(4);
1147+
Object obfuscatedProfileId = list.get(5);
11181148
pigeonResult.setObfuscatedProfileId((String) obfuscatedProfileId);
1119-
Object oldProduct = list.get(5);
1149+
Object oldProduct = list.get(6);
11201150
pigeonResult.setOldProduct((String) oldProduct);
1121-
Object purchaseToken = list.get(6);
1151+
Object purchaseToken = list.get(7);
11221152
pigeonResult.setPurchaseToken((String) purchaseToken);
11231153
return pigeonResult;
11241154
}

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ class MethodCallHandlerImpl implements Application.ActivityLifecycleCallbacks, I
6060
com.android.billingclient.api.BillingFlowParams.ProrationMode
6161
.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;
6262

63+
@VisibleForTesting
64+
static final int REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY =
65+
com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
66+
.UNKNOWN_REPLACEMENT_MODE;
67+
6368
private static final String TAG = "InAppPurchasePlugin";
6469
private static final String LOAD_PRODUCT_DOC_URL =
6570
"https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale";
@@ -285,9 +290,20 @@ public void queryProductDetailsAsync(
285290
}
286291
}
287292

293+
if (params.getProrationMode() != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
294+
&& params.getReplacementMode()
295+
!= REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
296+
throw new FlutterError(
297+
"IN_APP_PURCHASE_CONFLICT_PRORATION_MODE_REPLACEMENT_MODE",
298+
"launchBillingFlow failed because you provided both prorationMode and replacementMode. You can only provide one of them.",
299+
null);
300+
}
301+
288302
if (params.getOldProduct() == null
289-
&& params.getProrationMode()
290-
!= PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
303+
&& (params.getProrationMode()
304+
!= PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
305+
|| params.getReplacementMode()
306+
!= REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY)) {
291307
throw new FlutterError(
292308
"IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT",
293309
"launchBillingFlow failed because oldProduct is null. You must provide a valid oldProduct in order to use a proration mode.",
@@ -336,9 +352,16 @@ public void queryProductDetailsAsync(
336352
&& !params.getOldProduct().isEmpty()
337353
&& params.getPurchaseToken() != null) {
338354
subscriptionUpdateParamsBuilder.setOldPurchaseToken(params.getPurchaseToken());
339-
// Set the prorationMode using a helper to minimize impact of deprecation warning suppression.
340-
setReplaceProrationMode(
341-
subscriptionUpdateParamsBuilder, params.getProrationMode().intValue());
355+
if (params.getProrationMode()
356+
!= PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
357+
setReplaceProrationMode(
358+
subscriptionUpdateParamsBuilder, params.getProrationMode().intValue());
359+
}
360+
if (params.getReplacementMode()
361+
!= REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) {
362+
subscriptionUpdateParamsBuilder.setSubscriptionReplacementMode(
363+
params.getReplacementMode().intValue());
364+
}
342365
paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build());
343366
}
344367
return fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build()));
@@ -385,7 +408,8 @@ public void queryPurchasesAsync(
385408
}
386409

387410
try {
388-
// Like in our connect call, consider the billing client responding a "success" here regardless
411+
// Like in our connect call, consider the billing client responding a "success" here
412+
// regardless
389413
// of status code.
390414
QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder();
391415
paramsBuilder.setProductType(toProductTypeString(productType));

packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE;
88
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;
9+
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;
910
import static java.util.Arrays.asList;
1011
import static java.util.Collections.singletonList;
1112
import static java.util.Collections.unmodifiableList;
@@ -556,6 +557,8 @@ public void launchBillingFlow_null_AccountId_do_not_crash() {
556557
paramsBuilder.setProduct(productId);
557558
paramsBuilder.setProrationMode(
558559
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
560+
paramsBuilder.setReplacementMode(
561+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
559562

560563
// Launch the billing flow
561564
BillingResult billingResult = buildBillingResult();
@@ -581,6 +584,8 @@ public void launchBillingFlow_ok_null_OldProduct() {
581584
paramsBuilder.setAccountId(accountId);
582585
paramsBuilder.setProrationMode(
583586
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
587+
paramsBuilder.setReplacementMode(
588+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
584589

585590
// Launch the billing flow
586591
BillingResult billingResult = buildBillingResult();
@@ -610,6 +615,8 @@ public void launchBillingFlow_ok_null_Activity() {
610615
paramsBuilder.setAccountId(accountId);
611616
paramsBuilder.setProrationMode(
612617
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
618+
paramsBuilder.setReplacementMode(
619+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
613620

614621
// Assert that the synchronous call throws an exception.
615622
FlutterError exception =
@@ -633,6 +640,8 @@ public void launchBillingFlow_ok_oldProduct() {
633640
paramsBuilder.setOldProduct(oldProductId);
634641
paramsBuilder.setProrationMode(
635642
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
643+
paramsBuilder.setReplacementMode(
644+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
636645

637646
// Launch the billing flow
638647
BillingResult billingResult = buildBillingResult();
@@ -660,6 +669,8 @@ public void launchBillingFlow_ok_AccountId() {
660669
paramsBuilder.setAccountId(accountId);
661670
paramsBuilder.setProrationMode(
662671
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
672+
paramsBuilder.setReplacementMode(
673+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
663674

664675
// Launch the billing flow
665676
BillingResult billingResult = buildBillingResult();
@@ -695,6 +706,8 @@ public void launchBillingFlow_ok_Proration() {
695706
paramsBuilder.setOldProduct(oldProductId);
696707
paramsBuilder.setPurchaseToken(purchaseToken);
697708
paramsBuilder.setProrationMode((long) prorationMode);
709+
paramsBuilder.setReplacementMode(
710+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
698711

699712
// Launch the billing flow
700713
BillingResult billingResult = buildBillingResult();
@@ -728,6 +741,8 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() {
728741
paramsBuilder.setAccountId(accountId);
729742
paramsBuilder.setOldProduct(null);
730743
paramsBuilder.setProrationMode((long) prorationMode);
744+
paramsBuilder.setReplacementMode(
745+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
731746

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

762+
@Test
763+
@SuppressWarnings(value = "deprecation")
764+
public void launchBillingFlow_ok_Replacement_with_null_OldProduct() {
765+
// Fetch the product details first and query the method call
766+
String productId = "foo";
767+
String accountId = "account";
768+
String queryOldProductId = "oldFoo";
769+
int replacementMode =
770+
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE;
771+
queryForProducts(unmodifiableList(asList(productId, queryOldProductId)));
772+
PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder();
773+
paramsBuilder.setProduct(productId);
774+
paramsBuilder.setAccountId(accountId);
775+
paramsBuilder.setOldProduct(null);
776+
paramsBuilder.setProrationMode(
777+
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
778+
paramsBuilder.setReplacementMode((long) replacementMode);
779+
780+
// Launch the billing flow
781+
BillingResult billingResult = buildBillingResult();
782+
when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult);
783+
784+
// Assert that the synchronous call throws an exception.
785+
FlutterError exception =
786+
assertThrows(
787+
FlutterError.class,
788+
() -> methodChannelHandler.launchBillingFlow(paramsBuilder.build()));
789+
assertEquals("IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", exception.code);
790+
assertTrue(
791+
Objects.requireNonNull(exception.getMessage())
792+
.contains("launchBillingFlow failed because oldProduct is null"));
793+
}
794+
795+
@Test
796+
@SuppressWarnings(value = "deprecation")
797+
public void launchBillingFlow_ok_Proration_and_Replacement_conflict() {
798+
// Fetch the product details first and query the method call
799+
String productId = "foo";
800+
String accountId = "account";
801+
String queryOldProductId = "oldFoo";
802+
int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE;
803+
int replacementMode =
804+
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE;
805+
queryForProducts(unmodifiableList(asList(productId, queryOldProductId)));
806+
PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder();
807+
paramsBuilder.setProduct(productId);
808+
paramsBuilder.setAccountId(accountId);
809+
paramsBuilder.setOldProduct(queryOldProductId);
810+
paramsBuilder.setProrationMode((long) prorationMode);
811+
paramsBuilder.setReplacementMode((long) replacementMode);
812+
813+
// Launch the billing flow
814+
BillingResult billingResult = buildBillingResult();
815+
when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult);
816+
817+
// Assert that the synchronous call throws an exception.
818+
FlutterError exception =
819+
assertThrows(
820+
FlutterError.class,
821+
() -> methodChannelHandler.launchBillingFlow(paramsBuilder.build()));
822+
assertEquals("IN_APP_PURCHASE_CONFLICT_PRORATION_MODE_REPLACEMENT_MODE", exception.code);
823+
assertTrue(
824+
Objects.requireNonNull(exception.getMessage())
825+
.contains(
826+
"launchBillingFlow failed because you provided both prorationMode and replacementMode. You can only provide one of them."));
827+
}
828+
747829
// TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new
748830
// ReplacementMode enum values.
749831
// https://github.com/flutter/flutter/issues/128957.
@@ -763,6 +845,8 @@ public void launchBillingFlow_ok_Full() {
763845
paramsBuilder.setOldProduct(oldProductId);
764846
paramsBuilder.setPurchaseToken(purchaseToken);
765847
paramsBuilder.setProrationMode((long) prorationMode);
848+
paramsBuilder.setReplacementMode(
849+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
766850

767851
// Launch the billing flow
768852
BillingResult billingResult = buildBillingResult();
@@ -790,6 +874,8 @@ public void launchBillingFlow_clientDisconnected() {
790874
paramsBuilder.setAccountId(accountId);
791875
paramsBuilder.setProrationMode(
792876
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
877+
paramsBuilder.setReplacementMode(
878+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
793879

794880
// Assert that the synchronous call throws an exception.
795881
FlutterError exception =
@@ -811,6 +897,8 @@ public void launchBillingFlow_productNotFound() {
811897
paramsBuilder.setAccountId(accountId);
812898
paramsBuilder.setProrationMode(
813899
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
900+
paramsBuilder.setReplacementMode(
901+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
814902

815903
// Assert that the synchronous call throws an exception.
816904
FlutterError exception =
@@ -835,6 +923,8 @@ public void launchBillingFlow_oldProductNotFound() {
835923
paramsBuilder.setOldProduct(oldProductId);
836924
paramsBuilder.setProrationMode(
837925
(long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
926+
paramsBuilder.setReplacementMode(
927+
(long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY);
838928

839929
// Assert that the synchronous call throws an exception.
840930
FlutterError exception =

0 commit comments

Comments
 (0)