diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 0dbc2427ccd6..535295a2f8af 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.5.0 + +* Migrate to Google Billing Library 3.0 + * Add `obfuscatedProfileId`, `purchaseToken` in [BillingClientWrapper.launchBillingFlow]. + * **Breaking Change** + * Removed `developerPayload` in [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase]. + * Removed `isRewarded` from [SkuDetailsWrapper]. + * [SkuDetailsWrapper.introductoryPriceCycles] now returns `int` instead of `String`. + * Above breaking changes are inline with the breaking changes introduced in [Google Play Billing 3.0 release](https://developer.android.com/google/play/billing/release-notes#3-0). + * Additional information on some the changes: + * [Dropping reward SKU support](https://support.google.com/googleplay/android-developer/answer/9155268?hl=en) + * [Developer payload](https://developer.android.com/google/play/billing/developer-payload) + ## 0.4.1 * Support InApp subscription upgrade/downgrade. diff --git a/packages/in_app_purchase/android/build.gradle b/packages/in_app_purchase/android/build.gradle index 2539f507ed26..8d5840b4daff 100644 --- a/packages/in_app_purchase/android/build.gradle +++ b/packages/in_app_purchase/android/build.gradle @@ -35,9 +35,9 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.0' - implementation 'com.android.billingclient:billing:2.0.3' + implementation 'com.android.billingclient:billing:3.0.2' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' + testImplementation 'org.mockito:mockito-core:3.6.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 58d077673a03..d90fc6040454 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -125,7 +125,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { launchBillingFlow( (String) call.argument("sku"), (String) call.argument("accountId"), + (String) call.argument("obfuscatedProfileId"), (String) call.argument("oldSku"), + (String) call.argument("purchaseToken"), call.hasArgument("prorationMode") ? (int) call.argument("prorationMode") : ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, @@ -138,16 +140,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { queryPurchaseHistoryAsync((String) call.argument("skuType"), result); break; case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC: - consumeAsync( - (String) call.argument("purchaseToken"), - (String) call.argument("developerPayload"), - result); + consumeAsync((String) call.argument("purchaseToken"), result); break; case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE: - acknowledgePurchase( - (String) call.argument("purchaseToken"), - (String) call.argument("developerPayload"), - result); + acknowledgePurchase((String) call.argument("purchaseToken"), result); break; default: result.notImplemented(); @@ -200,7 +196,9 @@ public void onSkuDetailsResponse( private void launchBillingFlow( String sku, @Nullable String accountId, + @Nullable String obfuscatedProfileId, @Nullable String oldSku, + @Nullable String purchaseToken, int prorationMode, MethodChannel.Result result) { if (billingClientError(result)) { @@ -248,10 +246,13 @@ private void launchBillingFlow( BillingFlowParams.Builder paramsBuilder = BillingFlowParams.newBuilder().setSkuDetails(skuDetails); if (accountId != null && !accountId.isEmpty()) { - paramsBuilder.setAccountId(accountId); + paramsBuilder.setObfuscatedAccountId(accountId); + } + if (obfuscatedProfileId != null && !obfuscatedProfileId.isEmpty()) { + paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId); } if (oldSku != null && !oldSku.isEmpty()) { - paramsBuilder.setOldSku(oldSku); + paramsBuilder.setOldSku(oldSku, purchaseToken); } // The proration mode value has to match one of the following declared in // https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode @@ -261,8 +262,7 @@ private void launchBillingFlow( billingClient.launchBillingFlow(activity, paramsBuilder.build()))); } - private void consumeAsync( - String purchaseToken, String developerPayload, final MethodChannel.Result result) { + private void consumeAsync(String purchaseToken, final MethodChannel.Result result) { if (billingClientError(result)) { return; } @@ -277,9 +277,6 @@ public void onConsumeResponse(BillingResult billingResult, String outToken) { ConsumeParams.Builder paramsBuilder = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); - if (developerPayload != null) { - paramsBuilder.setDeveloperPayload(developerPayload); - } ConsumeParams params = paramsBuilder.build(); billingClient.consumeAsync(params, listener); @@ -348,16 +345,12 @@ public void onBillingServiceDisconnected() { }); } - private void acknowledgePurchase( - String purchaseToken, @Nullable String developerPayload, final MethodChannel.Result result) { + private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) { if (billingClientError(result)) { return; } AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder() - .setDeveloperPayload(developerPayload) - .setPurchaseToken(purchaseToken) - .build(); + AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); billingClient.acknowledgePurchase( params, new AcknowledgePurchaseResponseListener() { diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 80b6f1362255..73180ec5ec05 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -31,7 +31,6 @@ static HashMap fromSkuDetail(SkuDetails detail) { info.put("priceCurrencyCode", detail.getPriceCurrencyCode()); info.put("sku", detail.getSku()); info.put("type", detail.getType()); - info.put("isRewarded", detail.isRewarded()); info.put("subscriptionPeriod", detail.getSubscriptionPeriod()); info.put("originalPrice", detail.getOriginalPrice()); info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros()); diff --git a/packages/in_app_purchase/example/android/app/build.gradle b/packages/in_app_purchase/example/android/app/build.gradle index 261c7f0fe58e..c95804685219 100644 --- a/packages/in_app_purchase/example/android/app/build.gradle +++ b/packages/in_app_purchase/example/android/app/build.gradle @@ -106,9 +106,9 @@ flutter { } dependencies { - implementation 'com.android.billingclient:billing:1.2' + implementation 'com.android.billingclient:billing:3.0.2' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' + testImplementation 'org.mockito:mockito-core:3.6.0' testImplementation 'org.json:json:20180813' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index cc7bc4a9b9b1..eef43346f655 100644 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -22,6 +22,7 @@ import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; @@ -60,6 +61,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -79,7 +81,7 @@ public class MethodCallHandlerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); + MockitoAnnotations.openMocks(this); factory = (@NonNull Context context, @NonNull MethodChannel channel, @@ -261,14 +263,18 @@ public void querySkuDetailsAsync_clientDisconnected() { verify(result, never()).success(any()); } + // Test launchBillingFlow not crash if `accountId` is `null` + // Ideally, we should check if the `accountId` is null in the parameter; however, + // since PBL 3.0, the `accountId` variable is not public. @Test - public void launchBillingFlow_ok_null_AccountId() { + public void launchBillingFlow_null_AccountId_do_not_crash() { // Fetch the sku details first and then prepare the launch billing flow call String skuId = "foo"; queryForSkus(singletonList(skuId)); HashMap arguments = new HashMap<>(); arguments.put("sku", skuId); arguments.put("accountId", null); + arguments.put("obfuscatedProfileId", null); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); // Launch the billing flow @@ -286,7 +292,6 @@ public void launchBillingFlow_ok_null_AccountId() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertNull(params.getAccountId()); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -320,7 +325,6 @@ public void launchBillingFlow_ok_null_OldSku() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertEquals(params.getAccountId(), accountId); assertNull(params.getOldSku()); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -374,7 +378,6 @@ public void launchBillingFlow_ok_oldSku() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertEquals(params.getAccountId(), accountId); assertEquals(params.getOldSku(), oldSkuId); // Verify we pass the response code to result @@ -408,7 +411,6 @@ public void launchBillingFlow_ok_AccountId() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertEquals(params.getAccountId(), accountId); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -420,6 +422,7 @@ public void launchBillingFlow_ok_Proration() { // Fetch the sku details first and query the method call String skuId = "foo"; String oldSkuId = "oldFoo"; + String purchaseToken = "purchaseTokenFoo"; String accountId = "account"; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); @@ -427,6 +430,7 @@ public void launchBillingFlow_ok_Proration() { arguments.put("sku", skuId); arguments.put("accountId", accountId); arguments.put("oldSku", oldSkuId); + arguments.put("purchaseToken", purchaseToken); arguments.put("prorationMode", prorationMode); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -445,8 +449,8 @@ public void launchBillingFlow_ok_Proration() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertEquals(params.getAccountId(), accountId); assertEquals(params.getOldSku(), oldSkuId); + assertEquals(params.getOldSkuPurchaseToken(), purchaseToken); assertEquals(params.getReplaceSkusProrationMode(), prorationMode); // Verify we pass the response code to result @@ -668,11 +672,7 @@ public void consumeAsync() { methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); - ConsumeParams params = - ConsumeParams.newBuilder() - .setDeveloperPayload("mockPayload") - .setPurchaseToken("mockToken") - .build(); + ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken("mockToken").build(); // Verify we pass the data to result verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); @@ -703,10 +703,7 @@ public void acknowledgePurchase() { methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder() - .setDeveloperPayload("mockPayload") - .setPurchaseToken("mockToken") - .build(); + AcknowledgePurchaseParams.newBuilder().setPurchaseToken("mockToken").build(); // Verify we pass the data to result verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); @@ -774,6 +771,7 @@ private void queryForSkus(List skusList) { verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); List skuDetailsResponse = skusList.stream().map(this::buildSkuDetails).collect(toList()); + BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) @@ -783,8 +781,16 @@ private void queryForSkus(List skusList) { } private SkuDetails buildSkuDetails(String id) { - SkuDetails details = mock(SkuDetails.class); - when(details.getSku()).thenReturn(id); + String json = + String.format( + "{\"packageName\": \"dummyPackageName\",\"productId\":\"%s\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}", + id); + SkuDetails details = null; + try { + details = new SkuDetails(json); + } catch (JSONException e) { + fail("buildSkuDetails failed with JSONException " + e.toString()); + } return details; } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart index a0ba91556094..f14e8dbec6bd 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -155,8 +155,13 @@ class BillingClient { /// The [skuDetails] needs to have already been fetched in a [querySkuDetails] /// call. The [accountId] is an optional hashed string associated with the user /// that's unique to your app. It's used by Google to detect unusual behavior. - /// Do not pass in a cleartext [accountId], use your developer ID, or use the - /// user's Google ID for this field. + /// Do not pass in a cleartext [accountId], and do not use this field to store any Personally Identifiable Information (PII) + /// such as emails in cleartext. Attempting to store PII in this field will result in purchases being blocked. + /// Google Play recommends that you use either encryption or a one-way hash to generate an obfuscated identifier to send to Google Play. + /// + /// Specifies an optional [obfuscatedProfileId] that is uniquely associated with the user's profile in your app. + /// Some applications allow users to have multiple profiles within a single account. Use this method to send the user's profile identifier to Google. + /// Setting this field requests the user's obfuscated account id. /// /// Calling this attemps to show the Google Play purchase UI. The user is free /// to complete the transaction there. @@ -169,27 +174,33 @@ class BillingClient { /// [`BillingClient#launchBillingFlow`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchbillingflow). /// It constructs a /// [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) - /// instance by [setting the given - /// skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails) - /// and [the given - /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)). + /// instance by [setting the given skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails), + /// [the given accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)) + /// and the [obfuscatedProfileId] (https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid). /// /// When this method is called to purchase a subscription, an optional `oldSku` /// can be passed in. This will tell Google Play that rather than purchasing a new subscription, /// the user needs to upgrade/downgrade the existing subscription. - /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) is the SKU id that the user is upgrading or downgrading from. + /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) and [purchaseToken] are the SKU id and purchase token that the user is upgrading or downgrading from. + /// [purchaseToken] must not be `null` if [oldSku] is not `null`. /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setreplaceskusprorationmode) is the mode of proration during subscription upgrade/downgrade. /// This value will only be effective if the `oldSku` is also set. Future launchBillingFlow( {required String sku, String? accountId, + String? obfuscatedProfileId, String? oldSku, + String? purchaseToken, ProrationMode? prorationMode}) async { assert(sku != null); + assert((oldSku == null) == (purchaseToken == null), + 'oldSku and purchaseToken must both be set, or both be null.'); final Map arguments = { 'sku': sku, 'accountId': accountId, + 'obfuscatedProfileId': obfuscatedProfileId, 'oldSku': oldSku, + 'purchaseToken': purchaseToken, 'prorationMode': ProrationModeConverter().toJson(prorationMode ?? ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) }; @@ -250,18 +261,14 @@ class BillingClient { /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. /// - /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. - /// /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) - Future consumeAsync(String purchaseToken, - {String? developerPayload}) async { + Future consumeAsync(String purchaseToken) async { assert(purchaseToken != null); return BillingResultWrapper.fromJson((await channel .invokeMapMethod( 'BillingClient#consumeAsync(String, ConsumeResponseListener)', { 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, })) ?? {}); } @@ -282,18 +289,14 @@ class BillingClient { /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more /// details. /// - /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. - /// /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) - Future acknowledgePurchase(String purchaseToken, - {String? developerPayload}) async { + Future acknowledgePurchase(String purchaseToken) async { assert(purchaseToken != null); return BillingResultWrapper.fromJson((await channel.invokeMapMethod( 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', { 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, })) ?? {}); } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart index 05472278968a..929b58292a2f 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -120,6 +120,8 @@ class PurchaseWrapper { /// The payload specified by the developer when the purchase was acknowledged or consumed. /// /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + /// The `developerPayload` is removed from [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase] + /// after plugin version `0.5.0`. As a result, this will be `null` for new purchases that happen after updating to `0.5.0`. final String? developerPayload; /// Whether the purchase has been acknowledged. diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart index b3872958e5b9..f93dd60284f8 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -42,7 +42,6 @@ class SkuDetailsWrapper { required this.subscriptionPeriod, required this.title, required this.type, - required this.isRewarded, required this.originalPrice, required this.originalPriceAmountMicros, }); @@ -71,9 +70,10 @@ class SkuDetailsWrapper { @JsonKey(defaultValue: '') final String introductoryPriceMicros; - /// The number of billing perios that [introductoryPrice] is valid for ("2"). - @JsonKey(defaultValue: '') - final String introductoryPriceCycles; + /// The number of subscription billing periods for which the user will be given the introductory price, such as 3. + /// Returns 0 if the SKU is not a subscription or doesn't have an introductory period. + @JsonKey(defaultValue: 0) + final int introductoryPriceCycles; /// The billing period of [introductoryPrice], in ISO 8601 format. @JsonKey(defaultValue: '') @@ -106,10 +106,6 @@ class SkuDetailsWrapper { /// The [SkuType] of the product. final SkuType type; - /// False if the product is paid. - @JsonKey(defaultValue: false) - final bool isRewarded; - /// The original price that the user purchased this product for. @JsonKey(defaultValue: '') final String originalPrice; @@ -138,7 +134,6 @@ class SkuDetailsWrapper { typedOther.subscriptionPeriod == subscriptionPeriod && typedOther.title == title && typedOther.type == type && - typedOther.isRewarded == isRewarded && typedOther.originalPrice == originalPrice && typedOther.originalPriceAmountMicros == originalPriceAmountMicros; } @@ -158,7 +153,6 @@ class SkuDetailsWrapper { subscriptionPeriod.hashCode, title.hashCode, type.hashCode, - isRewarded.hashCode, originalPrice, originalPriceAmountMicros); } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 247dbd54b666..a14affdf9ed3 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -12,7 +12,7 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', introductoryPrice: json['introductoryPrice'] as String? ?? '', introductoryPriceMicros: json['introductoryPriceMicros'] as String? ?? '', - introductoryPriceCycles: json['introductoryPriceCycles'] as String? ?? '', + introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', price: json['price'] as String? ?? '', priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, @@ -21,7 +21,6 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', title: json['title'] as String? ?? '', type: const SkuTypeConverter().fromJson(json['type'] as String?), - isRewarded: json['isRewarded'] as bool? ?? false, originalPrice: json['originalPrice'] as String? ?? '', originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, ); @@ -42,7 +41,6 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => 'subscriptionPeriod': instance.subscriptionPeriod, 'title': instance.title, 'type': const SkuTypeConverter().toJson(instance.type), - 'isRewarded': instance.isRewarded, 'originalPrice': instance.originalPrice, 'originalPriceAmountMicros': instance.originalPriceAmountMicros, }; diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart index d4601fd809db..79a4a61fb328 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart @@ -84,8 +84,8 @@ class AppStoreConnection implements InAppPurchaseConnection { } @override - Future completePurchase(PurchaseDetails purchase, - {String? developerPayload}) async { + Future completePurchase( + PurchaseDetails purchase) async { if (purchase.skPaymentTransaction == null) { throw ArgumentError( 'completePurchase unsuccessful. The `purchase.skPaymentTransaction` is not valid'); @@ -96,8 +96,7 @@ class AppStoreConnection implements InAppPurchaseConnection { } @override - Future consumePurchase(PurchaseDetails purchase, - {String? developerPayload}) { + Future consumePurchase(PurchaseDetails purchase) { throw UnsupportedError('consume purchase is not available on Android'); } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart index 1a47f3ebd095..c45512ed353f 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart @@ -66,6 +66,8 @@ class GooglePlayConnection accountId: purchaseParam.applicationUserName, oldSku: purchaseParam .changeSubscriptionParam?.oldPurchaseDetails.productID, + purchaseToken: purchaseParam.changeSubscriptionParam + ?.oldPurchaseDetails.verificationData.serverVerificationData, prorationMode: purchaseParam.changeSubscriptionParam?.prorationMode); return billingResultWrapper.responseCode == BillingResponse.ok; @@ -81,8 +83,8 @@ class GooglePlayConnection } @override - Future completePurchase(PurchaseDetails purchase, - {String? developerPayload}) async { + Future completePurchase( + PurchaseDetails purchase) async { if (purchase.billingClientPurchase!.isAcknowledged) { return BillingResultWrapper(responseCode: BillingResponse.ok); } @@ -90,21 +92,18 @@ class GooglePlayConnection throw ArgumentError( 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return await billingClient.acknowledgePurchase( - purchase.verificationData.serverVerificationData, - developerPayload: developerPayload); + return await billingClient + .acknowledgePurchase(purchase.verificationData.serverVerificationData); } @override - Future consumePurchase(PurchaseDetails purchase, - {String? developerPayload}) { + Future consumePurchase(PurchaseDetails purchase) { if (purchase.verificationData == null) { throw ArgumentError( 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return billingClient.consumeAsync( - purchase.verificationData.serverVerificationData, - developerPayload: developerPayload); + return billingClient + .consumeAsync(purchase.verificationData.serverVerificationData); } @override diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart index 81a0e92cc591..aac5eae93e55 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart @@ -204,10 +204,7 @@ abstract class InAppPurchaseConnection { /// /// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. /// The [consumePurchase] acts as an implicit [completePurchase] on Android. - /// - /// The optional parameter `developerPayload` (defaults to `null`) only works on Android. - Future completePurchase(PurchaseDetails purchase, - {String? developerPayload}); + Future completePurchase(PurchaseDetails purchase); /// (Play only) Mark that the user has consumed a product. /// @@ -215,11 +212,8 @@ abstract class InAppPurchaseConnection { /// delivered. The user won't be able to buy the same product again until the /// purchase of the product is consumed. /// - /// The `developerPayload` (defaults to `null`) can be specified to be associated with this consumption. - /// /// This throws an [UnsupportedError] on iOS. - Future consumePurchase(PurchaseDetails purchase, - {String? developerPayload}); + Future consumePurchase(PurchaseDetails purchase); /// Query all previous purchases. /// diff --git a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart index 3aa62ddd96a1..7ba560257b39 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -196,15 +196,53 @@ void main() { ); final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; + final String profileId = "hashedProfileId"; expect( await billingClient.launchBillingFlow( - sku: skuDetails.sku, accountId: accountId), + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId), equals(expectedBillingResult)); Map arguments = stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], equals(accountId)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); + }); + + test( + 'Change subscription throws assertion error `oldSku` and `purchaseToken` has different nullability', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = 'hashedAccountId'; + final String profileId = 'hashedProfileId'; + + expect( + billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: dummyOldPurchase.sku, + purchaseToken: null), + throwsAssertionError); + + expect( + billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: null, + purchaseToken: dummyOldPurchase.purchaseToken), + throwsAssertionError); }); test( @@ -219,19 +257,25 @@ void main() { value: buildBillingResultMap(expectedBillingResult), ); final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + final String accountId = 'hashedAccountId'; + final String profileId = 'hashedProfileId'; expect( await billingClient.launchBillingFlow( sku: skuDetails.sku, accountId: accountId, - oldSku: dummyOldPurchase.sku), + obfuscatedProfileId: profileId, + oldSku: dummyOldPurchase.sku, + purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); Map arguments = stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], equals(accountId)); expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect( + arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); }); test( @@ -246,21 +290,27 @@ void main() { value: buildBillingResultMap(expectedBillingResult), ); final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + final String accountId = 'hashedAccountId'; + final String profileId = 'hashedProfileId'; final prorationMode = ProrationMode.immediateAndChargeProratedPrice; expect( await billingClient.launchBillingFlow( sku: skuDetails.sku, accountId: accountId, + obfuscatedProfileId: profileId, oldSku: dummyOldPurchase.sku, - prorationMode: prorationMode), + prorationMode: prorationMode, + purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); Map arguments = stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], equals(accountId)); expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); + expect( + arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); expect(arguments['prorationMode'], ProrationModeConverter().toJson(prorationMode)); }); @@ -440,8 +490,8 @@ void main() { name: consumeMethodName, value: buildBillingResultMap(expectedBillingResult)); - final BillingResultWrapper billingResult = await billingClient - .consumeAsync('dummy token', developerPayload: 'dummy payload'); + final BillingResultWrapper billingResult = + await billingClient.consumeAsync('dummy token'); expect(billingResult, equals(expectedBillingResult)); }); @@ -451,8 +501,8 @@ void main() { name: consumeMethodName, value: null, ); - final BillingResultWrapper billingResult = await billingClient - .consumeAsync('dummy token', developerPayload: 'dummy payload'); + final BillingResultWrapper billingResult = + await billingClient.consumeAsync('dummy token'); expect( billingResult, @@ -475,8 +525,7 @@ void main() { value: buildBillingResultMap(expectedBillingResult)); final BillingResultWrapper billingResult = - await billingClient.acknowledgePurchase('dummy token', - developerPayload: 'dummy payload'); + await billingClient.acknowledgePurchase('dummy token'); expect(billingResult, equals(expectedBillingResult)); }); @@ -486,8 +535,7 @@ void main() { value: null, ); final BillingResultWrapper billingResult = - await billingClient.acknowledgePurchase('dummy token', - developerPayload: 'dummy payload'); + await billingClient.acknowledgePurchase('dummy token'); expect( billingResult, diff --git a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart index 13715eeb9fc0..7a7b7fb86364 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -12,7 +12,7 @@ final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( freeTrialPeriod: 'freeTrialPeriod', introductoryPrice: 'introductoryPrice', introductoryPriceMicros: 'introductoryPriceMicros', - introductoryPriceCycles: 'introductoryPriceCycles', + introductoryPriceCycles: 1, introductoryPricePeriod: 'introductoryPricePeriod', price: 'price', priceAmountMicros: 1000, @@ -21,7 +21,6 @@ final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( subscriptionPeriod: 'subscriptionPeriod', title: 'title', type: SkuType.inapp, - isRewarded: true, originalPrice: 'originalPrice', originalPriceAmountMicros: 1000, ); @@ -144,7 +143,6 @@ Map buildSkuMap(SkuDetailsWrapper original) { 'subscriptionPeriod': original.subscriptionPeriod, 'title': original.title, 'type': original.type.toString().substring(8), - 'isRewarded': original.isRewarded, 'originalPrice': original.originalPrice, 'originalPriceAmountMicros': original.originalPriceAmountMicros, }; diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart index 79c2ee436c5c..5a265b8de907 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart @@ -632,9 +632,8 @@ void main() { purchaseDetails.status = PurchaseStatus.purchased; if (purchaseDetails.pendingCompletePurchase) { final BillingResultWrapper billingResultWrapper = - await GooglePlayConnection.instance.completePurchase( - purchaseDetails, - developerPayload: 'dummy payload'); + await GooglePlayConnection.instance + .completePurchase(purchaseDetails); expect(billingResultWrapper, equals(expectedBillingResult)); completer.complete(billingResultWrapper); }