From e4c979655ece3b573985565279355b524c7db908 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 21 Oct 2019 10:09:08 -0700 Subject: [PATCH 01/16] migrate to play library 2.0 --- packages/in_app_purchase/CHANGELOG.md | 19 ++ packages/in_app_purchase/android/build.gradle | 2 +- .../BillingClientFactoryImpl.java | 1 + .../inapppurchase/InAppPurchasePlugin.java | 2 + .../inapppurchase/MethodCallHandlerImpl.java | 78 ++++-- .../inapppurchase/PluginPurchaseListener.java | 7 +- .../plugins/inapppurchase/Translator.java | 40 ++++ .../inapppurchase/MethodCallHandlerTest.java | 176 +++++++++++--- .../plugins/inapppurchase/TranslatorTest.java | 93 ++++++-- .../in_app_purchase/example/lib/main.dart | 9 +- .../billing_client_wrapper.dart | 76 ++++-- .../enum_converters.dart | 33 +++ .../enum_converters.g.dart | 13 +- .../purchase_wrapper.dart | 224 ++++++++++++++++-- .../purchase_wrapper.g.dart | 52 +++- .../sku_details_wrapper.dart | 66 +++++- .../sku_details_wrapper.g.dart | 28 ++- .../in_app_purchase/app_store_connection.dart | 9 +- .../google_play_connection.dart | 75 ++++-- .../in_app_purchase_connection.dart | 28 ++- .../src/in_app_purchase/purchase_details.dart | 42 +++- .../billing_client_wrapper_test.dart | 172 ++++++++++---- .../purchase_wrapper_test.dart | 85 ++++++- .../sku_details_wrapper_test.dart | 30 ++- .../app_store_connection_test.dart | 2 +- .../google_play_connection_test.dart | 172 +++++++++++--- 26 files changed, 1279 insertions(+), 255 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 2624cadd785d..9e65fbb582a5 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,22 @@ +## 0.3.2 + +* Migrate the `Google Play Library` to 2.0.3. + * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. + * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. + * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. + * A `billingResult` field is added to the `PurchasesResultWrapper`. + * Other Updates to the "billing_client_wrappers": + * Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields. + * Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. + * **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. + * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed", it is implicitly acknowledged. + * Updates to the "InAppPurchaseConnection": + * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. + * **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. + * A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. + * Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes. + * Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update. + ## 0.2.2+2 * Include lifecycle dependency as a compileOnly one on Android to resolve diff --git a/packages/in_app_purchase/android/build.gradle b/packages/in_app_purchase/android/build.gradle index 54bd5e183713..5d577c259d1b 100644 --- a/packages/in_app_purchase/android/build.gradle +++ b/packages/in_app_purchase/android/build.gradle @@ -35,7 +35,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.0' - implementation 'com.android.billingclient:billing:1.2' + implementation 'com.android.billingclient:billing:2.0.3' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.17.0' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index 383fcabbb3c5..af17bb071fc6 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -14,6 +14,7 @@ final class BillingClientFactoryImpl implements BillingClientFactory { @Override public BillingClient createBillingClient(Context context, MethodChannel channel) { return BillingClient.newBuilder(context) + .enablePendingPurchases() .setListener(new PluginPurchaseListener(channel)) .build(); } diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index 33910eea9122..a9302d10df4e 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java @@ -36,6 +36,8 @@ static final class MethodNames { "BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)"; static final String CONSUME_PURCHASE_ASYNC = "BillingClient#consumeAsync(String, ConsumeResponseListener)"; + static final String ACKNOWLEDGE_PURCHASE = + "BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; private MethodNames() {}; } 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 2abcc4b5f634..affc0cf8fe85 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 @@ -4,7 +4,7 @@ package io.flutter.plugins.inapppurchase; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; +import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; @@ -13,11 +13,15 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; -import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; @@ -89,7 +93,16 @@ 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"), result); + consumeAsync( + (String) call.argument("purchaseToken"), + (String) call.argument("developerPayload"), + result); + break; + case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE: + acknowledgePurchase( + (String) call.argument("purchaseToken"), + (String) call.argument("developerPayload"), + result); break; default: result.notImplemented(); @@ -123,11 +136,12 @@ private void querySkuDetailsAsync( billingClient.querySkuDetailsAsync( params, new SkuDetailsResponseListener() { + @Override public void onSkuDetailsResponse( - int responseCode, @Nullable List skuDetailsList) { + BillingResult billingResult, List skuDetailsList) { updateCachedSkus(skuDetailsList); final Map skuDetailsResponse = new HashMap<>(); - skuDetailsResponse.put("responseCode", responseCode); + skuDetailsResponse.put("billingResult", Translator.fromBillingResult(billingResult)); skuDetailsResponse.put("skuDetailsList", fromSkuDetailsList(skuDetailsList)); result.success(skuDetailsResponse); } @@ -164,10 +178,13 @@ private void launchBillingFlow( if (accountId != null && !accountId.isEmpty()) { paramsBuilder.setAccountId(accountId); } - result.success(billingClient.launchBillingFlow(activity, paramsBuilder.build())); + result.success( + Translator.fromBillingResult( + billingClient.launchBillingFlow(activity, paramsBuilder.build()))); } - private void consumeAsync(String purchaseToken, final MethodChannel.Result result) { + private void consumeAsync( + String purchaseToken, String developerPayload, final MethodChannel.Result result) { if (billingClientError(result)) { return; } @@ -175,12 +192,19 @@ private void consumeAsync(String purchaseToken, final MethodChannel.Result resul ConsumeResponseListener listener = new ConsumeResponseListener() { @Override - public void onConsumeResponse( - @BillingClient.BillingResponse int responseCode, String outToken) { - result.success(responseCode); + public void onConsumeResponse(BillingResult billingResult, String outToken) { + result.success(Translator.fromBillingResult(billingResult)); } }; - billingClient.consumeAsync(purchaseToken, listener); + ConsumeParams.Builder paramsBuilder = + ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); + + if (developerPayload != null) { + paramsBuilder.setDeveloperPayload(developerPayload); + } + ConsumeParams params = paramsBuilder.build(); + + billingClient.consumeAsync(params, listener); } private void queryPurchases(String skuType, MethodChannel.Result result) { @@ -201,10 +225,12 @@ private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Resul skuType, new PurchaseHistoryResponseListener() { @Override - public void onPurchaseHistoryResponse(int responseCode, List purchasesList) { + public void onPurchaseHistoryResponse( + BillingResult billingResult, List purchasesList) { final Map serialized = new HashMap<>(); - serialized.put("responseCode", responseCode); - serialized.put("purchasesList", fromPurchasesList(purchasesList)); + serialized.put("billingResult", Translator.fromBillingResult(billingResult)); + serialized.put( + "purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList)); result.success(serialized); } }); @@ -220,14 +246,14 @@ private void startConnection(final int handle, final MethodChannel.Result result private boolean alreadyFinished = false; @Override - public void onBillingSetupFinished(int responseCode) { + public void onBillingSetupFinished(BillingResult billingResult) { if (alreadyFinished) { Log.d(TAG, "Tried to call onBilllingSetupFinished multiple times."); return; } alreadyFinished = true; // Consider the fact that we've finished a success, leave it to the Dart side to validate the responseCode. - result.success(responseCode); + result.success(Translator.fromBillingResult(billingResult)); } @Override @@ -239,6 +265,26 @@ public void onBillingServiceDisconnected() { }); } + private void acknowledgePurchase( + String purchaseToken, @Nullable String developerPayload, final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + AcknowledgePurchaseParams params = + AcknowledgePurchaseParams.newBuilder() + .setDeveloperPayload(developerPayload) + .setPurchaseToken(purchaseToken) + .build(); + billingClient.acknowledgePurchase( + params, + new AcknowledgePurchaseResponseListener() { + @Override + public void onAcknowledgePurchaseResponse(BillingResult billingResult) { + result.success(Translator.fromBillingResult(billingResult)); + } + }); + } + private void updateCachedSkus(@Nullable List skuDetailsList) { if (skuDetailsList == null) { return; diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java index db3260cb5a0c..20ab8ad92e65 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java @@ -4,9 +4,11 @@ package io.flutter.plugins.inapppurchase; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; import androidx.annotation.Nullable; +import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchasesUpdatedListener; import io.flutter.plugin.common.MethodChannel; @@ -22,9 +24,10 @@ class PluginPurchaseListener implements PurchasesUpdatedListener { } @Override - public void onPurchasesUpdated(int responseCode, @Nullable List purchases) { + public void onPurchasesUpdated(BillingResult billingResult, @Nullable List purchases) { final Map callbackArgs = new HashMap<>(); - callbackArgs.put("responseCode", responseCode); + callbackArgs.put("billingResult", fromBillingResult(billingResult)); + callbackArgs.put("responseCode", billingResult.getResponseCode()); callbackArgs.put("purchasesList", fromPurchasesList(purchases)); channel.invokeMethod(InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED, callbackArgs); } 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 4502b7d43612..80b6f1362255 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 @@ -5,8 +5,10 @@ package io.flutter.plugins.inapppurchase; import androidx.annotation.Nullable; +import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.Purchase.PurchasesResult; +import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.SkuDetails; import java.util.ArrayList; import java.util.Collections; @@ -31,6 +33,8 @@ static HashMap fromSkuDetail(SkuDetails detail) { 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()); return info; } @@ -57,6 +61,21 @@ static HashMap fromPurchase(Purchase purchase) { info.put("sku", purchase.getSku()); info.put("isAutoRenewing", purchase.isAutoRenewing()); info.put("originalJson", purchase.getOriginalJson()); + info.put("developerPayload", purchase.getDeveloperPayload()); + info.put("isAcknowledged", purchase.isAcknowledged()); + info.put("purchaseState", purchase.getPurchaseState()); + return info; + } + + static HashMap fromPurchaseHistoryRecord( + PurchaseHistoryRecord purchaseHistoryRecord) { + HashMap info = new HashMap<>(); + info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime()); + info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken()); + info.put("signature", purchaseHistoryRecord.getSignature()); + info.put("sku", purchaseHistoryRecord.getSku()); + info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload()); + info.put("originalJson", purchaseHistoryRecord.getOriginalJson()); return info; } @@ -72,10 +91,31 @@ static List> fromPurchasesList(@Nullable List return serialized; } + static List> fromPurchaseHistoryRecordList( + @Nullable List purchaseHistoryRecords) { + if (purchaseHistoryRecords == null) { + return Collections.emptyList(); + } + + List> serialized = new ArrayList<>(); + for (PurchaseHistoryRecord purchaseHistoryRecord : purchaseHistoryRecords) { + serialized.add(fromPurchaseHistoryRecord(purchaseHistoryRecord)); + } + return serialized; + } + static HashMap fromPurchasesResult(PurchasesResult purchasesResult) { HashMap info = new HashMap<>(); info.put("responseCode", purchasesResult.getResponseCode()); + info.put("billingResult", fromBillingResult(purchasesResult.getBillingResult())); info.put("purchasesList", fromPurchasesList(purchasesResult.getPurchasesList())); return info; } + + static HashMap fromBillingResult(BillingResult billingResult) { + HashMap info = new HashMap<>(); + info.put("responseCode", billingResult.getResponseCode()); + info.put("debugMessage", billingResult.getDebugMessage()); + return info; + } } 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 47bfc113c081..f032c9d89580 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 @@ -1,5 +1,6 @@ package io.flutter.plugins.inapppurchase; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.END_CONNECTION; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY; @@ -10,6 +11,8 @@ import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; +import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; @@ -21,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -31,14 +35,18 @@ import android.app.Activity; import android.content.Context; import androidx.annotation.Nullable; +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.BillingClient.BillingResponse; import com.android.billingclient.api.BillingClient.SkuType; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.Purchase.PurchasesResult; +import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; @@ -114,9 +122,14 @@ public void isReady_clientDisconnected() { public void startConnection() { ArgumentCaptor captor = mockStartConnection(); verify(result, never()).success(any()); - captor.getValue().onBillingSetupFinished(100); - - verify(result, times(1)).success(100); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + captor.getValue().onBillingSetupFinished(billingResult); + + verify(result, times(1)).success(fromBillingResult(billingResult)); } @Test @@ -130,11 +143,27 @@ public void startConnection_multipleCalls() { methodChannelHandler.onMethodCall(call, result); verify(result, never()).success(any()); - captor.getValue().onBillingSetupFinished(100); - captor.getValue().onBillingSetupFinished(200); - captor.getValue().onBillingSetupFinished(300); - - verify(result, times(1)).success(100); + BillingResult billingResult1 = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + BillingResult billingResult2 = + BillingResult.newBuilder() + .setResponseCode(200) + .setDebugMessage("dummy debug message") + .build(); + BillingResult billingResult3 = + BillingResult.newBuilder() + .setResponseCode(300) + .setDebugMessage("dummy debug message") + .build(); + + captor.getValue().onBillingSetupFinished(billingResult1); + captor.getValue().onBillingSetupFinished(billingResult2); + captor.getValue().onBillingSetupFinished(billingResult3); + + verify(result, times(1)).success(fromBillingResult(billingResult1)); verify(result, times(1)).success(any()); } @@ -190,11 +219,16 @@ public void querySkuDetailsAsync() { // Assert that we handed result BillingClient's response int responseCode = 200; List skuDetailsResponse = asList(buildSkuDetails("foo")); - listenerCaptor.getValue().onSkuDetailsResponse(responseCode, skuDetailsResponse); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); verify(result).success(resultCaptor.capture()); HashMap resultData = resultCaptor.getValue(); - assertEquals(resultData.get("responseCode"), responseCode); + assertEquals(resultData.get("billingResult"), fromBillingResult(billingResult)); assertEquals(resultData.get("skuDetailsList"), fromSkuDetailsList(skuDetailsResponse)); } @@ -229,8 +263,12 @@ public void launchBillingFlow_ok_nullAccountId() { MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); // Launch the billing flow - int responseCode = BillingResponse.OK; - when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(responseCode); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); methodChannelHandler.onMethodCall(launchCall, result); // Verify we pass the arguments to the billing flow @@ -243,7 +281,7 @@ public void launchBillingFlow_ok_nullAccountId() { // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(responseCode); + verify(result, times(1)).success(fromBillingResult(billingResult)); } @Test @@ -277,8 +315,12 @@ public void launchBillingFlow_ok_AccountId() { MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); // Launch the billing flow - int responseCode = BillingResponse.OK; - when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(responseCode); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); methodChannelHandler.onMethodCall(launchCall, result); // Verify we pass the arguments to the billing flow @@ -291,7 +333,7 @@ public void launchBillingFlow_ok_AccountId() { // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(responseCode); + verify(result, times(1)).success(fromBillingResult(billingResult)); } @Test @@ -335,9 +377,14 @@ public void launchBillingFlow_skuNotFound() { public void queryPurchases() { establishConnectedBillingClient(null, null); PurchasesResult purchasesResult = mock(PurchasesResult.class); - when(purchasesResult.getResponseCode()).thenReturn(BillingResponse.OK); Purchase purchase = buildPurchase("foo"); when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase)); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(purchasesResult.getBillingResult()).thenReturn(billingResult); when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult); HashMap arguments = new HashMap<>(); @@ -370,8 +417,12 @@ public void queryPurchaseHistoryAsync() { // Set up an established billing client and all our mocked responses establishConnectedBillingClient(null, null); ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - int responseCode = BillingResponse.OK; - List purchasesList = asList(buildPurchase("foo")); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + List purchasesList = asList(buildPurchaseHistoryRecord("foo")); HashMap arguments = new HashMap<>(); arguments.put("skuType", SkuType.INAPP); ArgumentCaptor listenerCaptor = @@ -383,11 +434,12 @@ public void queryPurchaseHistoryAsync() { // Verify we pass the data to result verify(mockBillingClient) .queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture()); - listenerCaptor.getValue().onPurchaseHistoryResponse(responseCode, purchasesList); + listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList); verify(result).success(resultCaptor.capture()); HashMap resultData = resultCaptor.getValue(); - assertEquals(responseCode, resultData.get("responseCode")); - assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); + assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); + assertEquals( + fromPurchaseHistoryRecordList(purchasesList), resultData.get("purchaseHistoryRecordList")); } @Test @@ -409,40 +461,89 @@ public void queryPurchaseHistoryAsync_clientDisconnected() { public void onPurchasesUpdatedListener() { PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); - int responseCode = BillingResponse.OK; + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); List purchasesList = asList(buildPurchase("foo")); ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); doNothing() .when(mockMethodChannel) .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); - listener.onPurchasesUpdated(responseCode, purchasesList); + listener.onPurchasesUpdated(billingResult, purchasesList); HashMap resultData = resultCaptor.getValue(); - assertEquals(responseCode, resultData.get("responseCode")); + assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); } @Test public void consumeAsync() { establishConnectedBillingClient(null, null); - ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResponse.class); - int responseCode = BillingResponse.OK; + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); HashMap arguments = new HashMap<>(); arguments.put("purchaseToken", "mockToken"); + arguments.put("developerPayload", "mockPayload"); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(ConsumeResponseListener.class); methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); + ConsumeParams params = + ConsumeParams.newBuilder() + .setDeveloperPayload("mockPayload") + .setPurchaseToken("mockToken") + .build(); + // Verify we pass the data to result - verify(mockBillingClient).consumeAsync(eq("mockToken"), listenerCaptor.capture()); + verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); - listenerCaptor.getValue().onConsumeResponse(responseCode, "mockToken"); + listenerCaptor.getValue().onConsumeResponse(billingResult, "mockToken"); verify(result).success(resultCaptor.capture()); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(responseCode); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + + @Test + public void acknowledgetPurchase() { + establishConnectedBillingClient(null, null); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + HashMap arguments = new HashMap<>(); + arguments.put("purchaseToken", "mockToken"); + arguments.put("developerPayload", "mockPayload"); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AcknowledgePurchaseResponseListener.class); + + methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); + + AcknowledgePurchaseParams params = + AcknowledgePurchaseParams.newBuilder() + .setDeveloperPayload("mockPayload") + .setPurchaseToken("mockToken") + .build(); + + // Verify we pass the data to result + verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); + + listenerCaptor.getValue().onAcknowledgePurchaseResponse(billingResult); + verify(result).success(resultCaptor.capture()); + + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); } private ArgumentCaptor mockStartConnection() { @@ -489,7 +590,12 @@ private void queryForSkus(List skusList) { verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); List skuDetailsResponse = skusList.stream().map(this::buildSkuDetails).collect(toList()); - listenerCaptor.getValue().onSkuDetailsResponse(BillingResponse.OK, skuDetailsResponse); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); } private SkuDetails buildSkuDetails(String id) { @@ -503,4 +609,10 @@ private Purchase buildPurchase(String orderId) { when(purchase.getOrderId()).thenReturn(orderId); return purchase; } + + private PurchaseHistoryRecord buildPurchaseHistoryRecord(String purchaseToken) { + PurchaseHistoryRecord purchase = mock(PurchaseHistoryRecord.class); + when(purchase.getPurchaseToken()).thenReturn(purchaseToken); + return purchase; + } } diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 639af24a9732..5b5ab4ff14f7 100644 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -8,9 +8,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.android.billingclient.api.BillingClient.BillingResponse; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.Purchase.PurchasesResult; +import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.SkuDetails; import java.util.Arrays; import java.util.Collections; @@ -22,9 +24,9 @@ public class TranslatorTest { private static final String SKU_DETAIL_EXAMPLE_JSON = - "{\"productId\":\"example\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\"}"; + "{\"productId\":\"example\",\"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}"; private static final String PURCHASE_EXAMPLE_JSON = - "{\"orderId\":\"foo\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\"}"; + "{\"orderId\":\"foo\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}"; @Test public void fromSkuDetail() throws JSONException { @@ -38,7 +40,7 @@ public void fromSkuDetail() throws JSONException { @Test public void fromSkuDetailsList() throws JSONException { final String SKU_DETAIL_EXAMPLE_2_JSON = - "{\"productId\":\"example2\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\"}"; + "{\"productId\":\"example2\",\"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}"; final List expected = Arrays.asList( new SkuDetails(SKU_DETAIL_EXAMPLE_JSON), new SkuDetails(SKU_DETAIL_EXAMPLE_2_JSON)); @@ -58,14 +60,43 @@ public void fromSkuDetailsList_null() { @Test public void fromPurchase() throws JSONException { final Purchase expected = new Purchase(PURCHASE_EXAMPLE_JSON, "signature"); - assertSerialized(expected, Translator.fromPurchase(expected)); } + @Test + public void fromPurchaseHistoryRecord() throws JSONException { + final PurchaseHistoryRecord expected = + new PurchaseHistoryRecord(PURCHASE_EXAMPLE_JSON, "signature"); + assertSerialized(expected, Translator.fromPurchaseHistoryRecord(expected)); + } + + @Test + public void fromPurchasesHistoryRecordList() throws JSONException { + final String purchase2Json = + "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}"; + final String signature = "signature"; + final List expected = + Arrays.asList( + new PurchaseHistoryRecord(PURCHASE_EXAMPLE_JSON, signature), + new PurchaseHistoryRecord(purchase2Json, signature)); + + final List> serialized = + Translator.fromPurchaseHistoryRecordList(expected); + + assertEquals(expected.size(), serialized.size()); + assertSerialized(expected.get(0), serialized.get(0)); + assertSerialized(expected.get(1), serialized.get(1)); + } + + @Test + public void fromPurchasesHistoryRecordList_null() { + assertEquals(Collections.emptyList(), Translator.fromPurchaseHistoryRecordList(null)); + } + @Test public void fromPurchasesList() throws JSONException { final String purchase2Json = - "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\"}"; + "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}"; final String signature = "signature"; final List expected = Arrays.asList( @@ -87,33 +118,44 @@ public void fromPurchasesList_null() { public void fromPurchasesResult() throws JSONException { PurchasesResult result = mock(PurchasesResult.class); final String purchase2Json = - "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\"}"; + "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}"; final String signature = "signature"; final List expectedPurchases = Arrays.asList( new Purchase(PURCHASE_EXAMPLE_JSON, signature), new Purchase(purchase2Json, signature)); when(result.getPurchasesList()).thenReturn(expectedPurchases); - when(result.getResponseCode()).thenReturn(BillingResponse.OK); - + when(result.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + BillingResult newBillingResult = + BillingResult.newBuilder() + .setDebugMessage("dummy debug message") + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build(); + when(result.getBillingResult()).thenReturn(newBillingResult); final HashMap serialized = Translator.fromPurchasesResult(result); - assertEquals(BillingResponse.OK, serialized.get("responseCode")); + assertEquals(BillingClient.BillingResponseCode.OK, serialized.get("responseCode")); List> serializedPurchases = (List>) serialized.get("purchasesList"); assertEquals(expectedPurchases.size(), serializedPurchases.size()); assertSerialized(expectedPurchases.get(0), serializedPurchases.get(0)); assertSerialized(expectedPurchases.get(1), serializedPurchases.get(1)); + + Map billingResultMap = (Map) serialized.get("billingResult"); + assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); + assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); } @Test - public void fromPurchasesResult_null() throws JSONException { - PurchasesResult result = mock(PurchasesResult.class); - when(result.getResponseCode()).thenReturn(BillingResponse.ERROR); - - final HashMap serialized = Translator.fromPurchasesResult(result); - - assertEquals(BillingResponse.ERROR, serialized.get("responseCode")); - assertEquals(Collections.emptyList(), serialized.get("purchasesList")); + public void fromBillingResult() throws JSONException { + BillingResult newBillingResult = + BillingResult.newBuilder() + .setDebugMessage("dummy debug message") + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build(); + Map billingResultMap = Translator.fromBillingResult(newBillingResult); + + assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); + assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); } private void assertSerialized(SkuDetails expected, Map serialized) { @@ -132,6 +174,9 @@ private void assertSerialized(SkuDetails expected, Map serialize assertEquals(expected.getSubscriptionPeriod(), serialized.get("subscriptionPeriod")); assertEquals(expected.getTitle(), serialized.get("title")); assertEquals(expected.getType(), serialized.get("type")); + assertEquals(expected.getOriginalPrice(), serialized.get("originalPrice")); + assertEquals( + expected.getOriginalPriceAmountMicros(), serialized.get("originalPriceAmountMicros")); } private void assertSerialized(Purchase expected, Map serialized) { @@ -142,5 +187,17 @@ private void assertSerialized(Purchase expected, Map serialized) assertEquals(expected.getSignature(), serialized.get("signature")); assertEquals(expected.getOriginalJson(), serialized.get("originalJson")); assertEquals(expected.getSku(), serialized.get("sku")); + assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload")); + assertEquals(expected.isAcknowledged(), serialized.get("isAcknowledged")); + assertEquals(expected.getPurchaseState(), serialized.get("purchaseState")); + } + + private void assertSerialized(PurchaseHistoryRecord expected, Map serialized) { + assertEquals(expected.getPurchaseTime(), serialized.get("purchaseTime")); + assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken")); + assertEquals(expected.getSignature(), serialized.get("signature")); + assertEquals(expected.getOriginalJson(), serialized.get("originalJson")); + assertEquals(expected.getSku(), serialized.get("sku")); + assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload")); } } diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 729fc0f77f46..025cc9513255 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -226,7 +226,7 @@ class _MyAppState extends State { // We recommend that you use your own server to verity the purchase data. Map purchases = Map.fromEntries(_purchases.map((PurchaseDetails purchase) { - if (Platform.isIOS) { + if (purchase.pendingCompletePurchase) { InAppPurchaseConnection.instance.completePurchase(purchase); } return MapEntry(purchase.productID, purchase); @@ -374,13 +374,14 @@ class _MyAppState extends State { _handleInvalidPurchase(purchaseDetails); } } - if (Platform.isIOS) { - InAppPurchaseConnection.instance.completePurchase(purchaseDetails); - } else if (Platform.isAndroid) { + if (Platform.isAndroid) { if (!kAutoConsume && purchaseDetails.productID == _kConsumableId) { InAppPurchaseConnection.instance.consumePurchase(purchaseDetails); } } + if (purchaseDetails.pendingCompletePurchase) { + InAppPurchaseConnection.instance.completePurchase(purchaseDetails); + } } }); } 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 6d7cd83eb0ad..3daaeaf0e60c 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 @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import '../../billing_client_wrappers.dart'; import '../channel.dart'; import 'purchase_wrapper.dart'; import 'sku_details_wrapper.dart'; @@ -76,19 +77,20 @@ class BillingClient { /// /// [onBillingServiceConnected] has been converted from a callback parameter /// to the Future result returned by this function. This returns the - /// `BillingClient.BillingResponse` `responseCode` of the connection result. + /// `BillingClient.BillingResultWrapper` describing the connection result. /// /// This triggers the creation of a new `BillingClient` instance in Java if /// one doesn't already exist. - Future startConnection( + Future startConnection( {@required OnBillingServiceDisconnected onBillingServiceDisconnected}) async { List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); - return BillingResponseConverter().fromJson(await channel.invokeMethod( - "BillingClient#startConnection(BillingClientStateListener)", - {'handle': disconnectCallbacks.length - 1})); + return BillingResultWrapper.fromJson(await channel + .invokeMapMethod( + "BillingClient#startConnection(BillingClientStateListener)", + {'handle': disconnectCallbacks.length - 1})); } /// Calls @@ -134,7 +136,7 @@ class BillingClient { /// Calling this attemps to show the Google Play purchase UI. The user is free /// to complete the transaction there. /// - /// This method returns a [BillingResponse] representing the initial attempt + /// This method returns a [BillingResultWrapper] representing the initial attempt /// to show the Google Play billing flow. Actual purchase updates are /// delivered via the [PurchasesUpdatedListener]. /// @@ -146,16 +148,17 @@ class BillingClient { /// 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)). - Future launchBillingFlow( + Future launchBillingFlow( {@required String sku, String accountId}) async { assert(sku != null); final Map arguments = { 'sku': sku, 'accountId': accountId, }; - return BillingResponseConverter().fromJson(await channel.invokeMethod( - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', - arguments)); + return BillingResultWrapper.fromJson( + await channel.invokeMapMethod( + 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', + arguments)); } /// Fetches recent purchases for the given [SkuType]. @@ -190,9 +193,9 @@ class BillingClient { /// This wraps [`BillingClient#queryPurchaseHistoryAsync(String skuType, /// PurchaseHistoryResponseListener /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync). - Future queryPurchaseHistory(SkuType skuType) async { + Future queryPurchaseHistory(SkuType skuType) async { assert(skuType != null); - return PurchasesResultWrapper.fromJson(await channel.invokeMapMethod( 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', {'skuType': SkuTypeConverter().toJson(skuType)})); @@ -201,15 +204,50 @@ class BillingClient { /// Consumes a given in-app product. /// /// 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 [BillingResponse]. + /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. + /// + /// The `params` must not be 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) async { - assert(purchaseToken != null); - return BillingResponseConverter().fromJson(await channel.invokeMethod( - 'BillingClient#consumeAsync(String, ConsumeResponseListener)', - {'purchaseToken': purchaseToken}, - )); + Future consumeAsync(ConsumeParams params) async { + assert(params != null); + return BillingResultWrapper.fromJson(await channel + .invokeMapMethod( + 'BillingClient#consumeAsync(String, ConsumeResponseListener)', + { + 'purchaseToken': params.purchaseToken, + 'developerPayload': params.developerPayload, + })); + } + + /// Acknowledge an In-App purchase. + /// + /// The developer is required to acknowledge that they have granted entitlement for all in-app purchases. + /// + /// Warning! The acknowledgement has to be happen within the 3 days of the purchase. + /// Failure to do so will result the purchase to be refunded. + /// + /// For consumable items, calling [consumeAsync] acts as an implicit acknowledgement. This method can also + /// be called for explicitly acknowledging a consumable purchase. + /// + /// Be sure to only acknowledge a purchase when the [PurchaseWrapper.purchaseState] is [PurchaseStateWrapper.purchased]. + /// + /// Please refer to https://developer.android.com/google/play/billing/billing_library_overview#acknowledge for more + /// details. + /// + /// The `params` must not be 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( + AcknowledgeParams params) async { + assert(params != null); + return BillingResultWrapper.fromJson(await channel.invokeMapMethod( + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', + { + 'purchaseToken': params.purchaseToken, + 'developerPayload': params.developerPayload, + })); } @visibleForTesting diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart index 5d0522135d99..1e81895438c3 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:in_app_purchase/billing_client_wrappers.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:json_annotation/json_annotation.dart'; part 'enum_converters.g.dart'; @@ -42,4 +43,36 @@ class SkuTypeConverter implements JsonConverter { class _SerializedEnums { BillingResponse response; SkuType type; + PurchaseStateWrapper purchaseState; +} + +/// Serializer for [PurchaseStateWrapper]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@PurchaseStateConverter()`. +class PurchaseStateConverter + implements JsonConverter { + const PurchaseStateConverter(); + + @override + PurchaseStateWrapper fromJson(int json) => _$enumDecode( + _$PurchaseStateWrapperEnumMap.cast(), + json); + + @override + int toJson(PurchaseStateWrapper object) => + _$PurchaseStateWrapperEnumMap[object]; + + PurchaseStatus toPurchaseStatus(PurchaseStateWrapper object) { + switch (object) { + case PurchaseStateWrapper.pending: + return PurchaseStatus.pending; + case PurchaseStateWrapper.purchased: + return PurchaseStatus.purchased; + case PurchaseStateWrapper.unspecified_state: + return PurchaseStatus.error; + } + + throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); + } } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart index ec8d57ba60e1..572eaa9df50b 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart @@ -9,13 +9,16 @@ part of 'enum_converters.dart'; _SerializedEnums _$_SerializedEnumsFromJson(Map json) { return _SerializedEnums() ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) - ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']); + ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) + ..purchaseState = + _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']); } Map _$_SerializedEnumsToJson(_SerializedEnums instance) => { 'response': _$BillingResponseEnumMap[instance.response], - 'type': _$SkuTypeEnumMap[instance.type] + 'type': _$SkuTypeEnumMap[instance.type], + 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState] }; T _$enumDecode(Map enumValues, dynamic source) { @@ -49,3 +52,9 @@ const _$SkuTypeEnumMap = { SkuType.inapp: 'inapp', SkuType.subs: 'subs' }; + +const _$PurchaseStateWrapperEnumMap = { + PurchaseStateWrapper.unspecified_state: 0, + PurchaseStateWrapper.purchased: 1, + PurchaseStateWrapper.pending: 2 +}; 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 e2bea9fc4d03..af191ce47fde 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 @@ -7,13 +7,14 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'enum_converters.dart'; import 'billing_client_wrapper.dart'; +import 'sku_details_wrapper.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to // rebuild and watch for further changes. part 'purchase_wrapper.g.dart'; -/// Data structure reprenting a succesful purchase. +/// Data structure representing a successful purchase. /// /// All purchase information should also be verified manually, with your /// server if at all possible. See ["Verify a @@ -21,18 +22,21 @@ part 'purchase_wrapper.g.dart'; /// /// This wraps [`com.android.billlingclient.api.Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase) @JsonSerializable() +@PurchaseStateConverter() class PurchaseWrapper { @visibleForTesting - PurchaseWrapper({ - @required this.orderId, - @required this.packageName, - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.isAutoRenewing, - @required this.originalJson, - }); + PurchaseWrapper( + {@required this.orderId, + @required this.packageName, + @required this.purchaseTime, + @required this.purchaseToken, + @required this.signature, + @required this.sku, + @required this.isAutoRenewing, + @required this.originalJson, + @required this.developerPayload, + @required this.isAcknowledged, + @required this.purchaseState}); factory PurchaseWrapper.fromJson(Map map) => _$PurchaseWrapperFromJson(map); @@ -48,12 +52,23 @@ class PurchaseWrapper { typedOther.signature == signature && typedOther.sku == sku && typedOther.isAutoRenewing == isAutoRenewing && - typedOther.originalJson == originalJson; + typedOther.originalJson == originalJson && + typedOther.isAcknowledged == isAcknowledged && + typedOther.purchaseState == purchaseState; } @override - int get hashCode => hashValues(orderId, packageName, purchaseTime, - purchaseToken, signature, sku, isAutoRenewing, originalJson); + int get hashCode => hashValues( + orderId, + packageName, + purchaseTime, + purchaseToken, + signature, + sku, + isAutoRenewing, + originalJson, + isAcknowledged, + purchaseState); /// The unique ID for this purchase. Corresponds to the Google Payments order /// ID. @@ -89,11 +104,87 @@ class PurchaseWrapper { /// Note though that verifying a purchase locally is inherently insecure (see /// the article for more details). final String originalJson; + + /// The payload specified by the developer when the purchase was acknowledged or consumed. + final String developerPayload; + + /// Whether the purchase has been acknowledged. + final bool isAcknowledged; + + /// The state of purchase. + final PurchaseStateWrapper purchaseState; +} + +/// Data structure representing a purchase history record. +/// +/// This class includes a subset of fields in [PurchaseWrapper]. +/// +/// This wraps [`com.android.billlingclient.api.PurchaseHistoryRecord`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord) +/// +/// * See also: [BillingClient.queryPurchaseHistory] for obtaining a [PurchaseHistoryRecordWrapper]. +// We can optionally make [PurchaseWrapper] extend or implement [PurchaseHistoryRecordWrapper]. +// For now, we keep them separated classes to be consistent with Android's BillingClient implementation. +@JsonSerializable() +class PurchaseHistoryRecordWrapper { + @visibleForTesting + PurchaseHistoryRecordWrapper({ + @required this.purchaseTime, + @required this.purchaseToken, + @required this.signature, + @required this.sku, + @required this.originalJson, + @required this.developerPayload, + }); + + factory PurchaseHistoryRecordWrapper.fromJson(Map map) => + _$PurchaseHistoryRecordWrapperFromJson(map); + + /// When the purchase was made, as an epoch timestamp. + final int purchaseTime; + + /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + final String purchaseToken; + + /// Signature of purchase data, signed with the developer's private key. Uses + /// RSASSA-PKCS1-v1_5. + final String signature; + + /// The product ID of this purchase. + final String sku; + + /// Details about this purchase, in JSON. + /// + /// This can be used verify a purchase. See ["Verify a purchase on a + /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). + /// Note though that verifying a purchase locally is inherently insecure (see + /// the article for more details). + final String originalJson; + + /// The payload specified by the developer when the purchase was acknowledged or consumed. + final String developerPayload; + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + if (other.runtimeType != runtimeType) return false; + final PurchaseHistoryRecordWrapper typedOther = other; + return typedOther.purchaseTime == purchaseTime && + typedOther.purchaseToken == purchaseToken && + typedOther.signature == signature && + typedOther.sku == sku && + typedOther.originalJson == originalJson && + typedOther.developerPayload == developerPayload; + } + + @override + int get hashCode => hashValues(purchaseTime, purchaseToken, signature, sku, + originalJson, developerPayload); } /// A data struct representing the result of a transaction. /// -/// Contains a potentially empty list of [PurchaseWrapper]s and a +/// Contains a potentially empty list of [PurchaseWrapper]s, a [BillingResultWrapper] +/// that contains a detailed description of the status and a /// [BillingResponse] to signify the overall state of the transaction. /// /// Wraps [`com.android.billingclient.api.Purchase.PurchasesResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult). @@ -102,6 +193,7 @@ class PurchaseWrapper { class PurchasesResultWrapper { PurchasesResultWrapper( {@required BillingResponse this.responseCode, + @required BillingResultWrapper this.billingResult, @required List this.purchasesList}); factory PurchasesResultWrapper.fromJson(Map map) => @@ -113,11 +205,15 @@ class PurchasesResultWrapper { if (other.runtimeType != runtimeType) return false; final PurchasesResultWrapper typedOther = other; return typedOther.responseCode == responseCode && - typedOther.purchasesList == purchasesList; + typedOther.purchasesList == purchasesList && + typedOther.billingResult == billingResult; } @override - int get hashCode => hashValues(responseCode, purchasesList); + int get hashCode => hashValues(billingResult, responseCode, purchasesList); + + /// The detailed description of the status of the operation. + final BillingResultWrapper billingResult; /// The status of the operation. /// @@ -125,8 +221,100 @@ class PurchasesResultWrapper { /// of the operation and the "user made purchases" transaction itself. final BillingResponse responseCode; - /// The list of succesful purchases made in this transaction. + /// The list of successful purchases made in this transaction. /// /// May be empty, especially if [responseCode] is not [BillingResponse.ok]. final List purchasesList; } + +/// A data struct representing the result of a purchase history. +/// +/// Contains a potentially empty list of [PurchaseHistoryRecordWrapper]s and a [BillingResultWrapper] +/// that contains a detailed description of the status. +@JsonSerializable() +@BillingResponseConverter() +class PurchasesHistoryResult { + PurchasesHistoryResult( + {@required + BillingResultWrapper this.billingResult, + @required + List this.purchaseHistoryRecordList}); + + factory PurchasesHistoryResult.fromJson(Map map) => + _$PurchasesHistoryResultFromJson(map); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + if (other.runtimeType != runtimeType) return false; + final PurchasesHistoryResult typedOther = other; + return typedOther.purchaseHistoryRecordList == purchaseHistoryRecordList && + typedOther.billingResult == billingResult; + } + + @override + int get hashCode => hashValues(billingResult, purchaseHistoryRecordList); + + /// The detailed description of the status of the [BillingClient.queryPurchaseHistory]. + final BillingResultWrapper billingResult; + + /// The list of queried purchase history records. + /// + /// May be empty, especially if [billingResult.responseCode] is not [BillingResponse.ok]. + final List purchaseHistoryRecordList; +} + +/// The parameter object used when consuming a purchase. +/// +/// See also [BillingClient.consumeAsync] for consuming a purchase. +class ConsumeParams { + /// Constructs the [ConsumeParams]. + /// + /// The `purchaseToken` must not be null. + /// The default value of `developerPayload` is null. + ConsumeParams({@required this.purchaseToken, this.developerPayload = null}); + + /// The developer data associated with the purchase to be consumed. + /// + /// Defaults to null. + final String developerPayload; + + /// The token that identifies the purchase to be consumed. + final String purchaseToken; +} + +/// The parameter object used when acknowledge a purchase. +/// +/// See also [BillingClient.acknowledgePurchase] for acknowledging a purchase. +class AcknowledgeParams { + /// Constructs the [AcknowledgeParams]. + /// + /// The `purchaseToken` must not be null. + /// The default value of `developerPayload` is null. + AcknowledgeParams( + {@required this.purchaseToken, this.developerPayload = null}); + + /// The developer data associated with the purchase to be acknowledged. + /// + /// Defaults to null. + final String developerPayload; + + /// The token that identifies the purchase to be acknowledged. + final String purchaseToken; +} + +/// Possible state of a [PurchaseWrapper]. +/// +/// Wraps +/// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html). +/// * See also: [PurchaseWrapper]. +enum PurchaseStateWrapper { + @JsonValue(0) + unspecified_state, + + @JsonValue(1) + purchased, + + @JsonValue(2) + pending, +} diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index 4af4b6992587..062d25db1f88 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -15,7 +15,11 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) { signature: json['signature'] as String, sku: json['sku'] as String, isAutoRenewing: json['isAutoRenewing'] as bool, - originalJson: json['originalJson'] as String); + originalJson: json['originalJson'] as String, + developerPayload: json['developerPayload'] as String, + isAcknowledged: json['isAcknowledged'] as bool, + purchaseState: const PurchaseStateConverter() + .fromJson(json['purchaseState'] as int)); } Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => @@ -27,13 +31,40 @@ Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => 'signature': instance.signature, 'sku': instance.sku, 'isAutoRenewing': instance.isAutoRenewing, - 'originalJson': instance.originalJson + 'originalJson': instance.originalJson, + 'developerPayload': instance.developerPayload, + 'isAcknowledged': instance.isAcknowledged, + 'purchaseState': + const PurchaseStateConverter().toJson(instance.purchaseState) + }; + +PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { + return PurchaseHistoryRecordWrapper( + purchaseTime: json['purchaseTime'] as int, + purchaseToken: json['purchaseToken'] as String, + signature: json['signature'] as String, + sku: json['sku'] as String, + originalJson: json['originalJson'] as String, + developerPayload: json['developerPayload'] as String); +} + +Map _$PurchaseHistoryRecordWrapperToJson( + PurchaseHistoryRecordWrapper instance) => + { + 'purchaseTime': instance.purchaseTime, + 'purchaseToken': instance.purchaseToken, + 'signature': instance.signature, + 'sku': instance.sku, + 'originalJson': instance.originalJson, + 'developerPayload': instance.developerPayload }; PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { return PurchasesResultWrapper( responseCode: const BillingResponseConverter() .fromJson(json['responseCode'] as int), + billingResult: + BillingResultWrapper.fromJson(json['billingResult'] as Map), purchasesList: (json['purchasesList'] as List) .map((e) => PurchaseWrapper.fromJson(e as Map)) .toList()); @@ -42,7 +73,24 @@ PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { Map _$PurchasesResultWrapperToJson( PurchasesResultWrapper instance) => { + 'billingResult': instance.billingResult, 'responseCode': const BillingResponseConverter().toJson(instance.responseCode), 'purchasesList': instance.purchasesList }; + +PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) { + return PurchasesHistoryResult( + billingResult: + BillingResultWrapper.fromJson(json['billingResult'] as Map), + purchaseHistoryRecordList: (json['purchaseHistoryRecordList'] as List) + .map((e) => PurchaseHistoryRecordWrapper.fromJson(e as Map)) + .toList()); +} + +Map _$PurchasesHistoryResultToJson( + PurchasesHistoryResult instance) => + { + 'billingResult': instance.billingResult, + 'purchaseHistoryRecordList': instance.purchaseHistoryRecordList + }; 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 670bf5125491..4bce2e5e9f37 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 @@ -35,6 +35,8 @@ class SkuDetailsWrapper { @required this.title, @required this.type, @required this.isRewarded, + @required this.originalPrice, + @required this.originalPriceAmountMicros, }); /// Constructs an instance of this from a key value map of data. @@ -84,6 +86,12 @@ class SkuDetailsWrapper { /// False if the product is paid. final bool isRewarded; + /// The original price that the user purchased this product for. + final String originalPrice; + + /// [originalPrice] in micro-units ("990000"). + final int originalPriceAmountMicros; + @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) { @@ -104,7 +112,9 @@ class SkuDetailsWrapper { typedOther.subscriptionPeriod == subscriptionPeriod && typedOther.title == title && typedOther.type == type && - typedOther.isRewarded == isRewarded; + typedOther.isRewarded == isRewarded && + typedOther.originalPrice == originalPrice && + typedOther.originalPriceAmountMicros == originalPriceAmountMicros; } @override @@ -122,7 +132,9 @@ class SkuDetailsWrapper { subscriptionPeriod.hashCode, title.hashCode, type.hashCode, - isRewarded.hashCode); + isRewarded.hashCode, + originalPrice, + originalPriceAmountMicros); } } @@ -130,10 +142,10 @@ class SkuDetailsWrapper { /// /// Returned by [BillingClient.querySkuDetails]. @JsonSerializable() -@BillingResponseConverter() class SkuDetailsResponseWrapper { @visibleForTesting - SkuDetailsResponseWrapper({@required this.responseCode, this.skuDetailsList}); + SkuDetailsResponseWrapper( + {@required this.billingResult, this.skuDetailsList}); /// Constructs an instance of this from a key value map of data. /// @@ -142,8 +154,8 @@ class SkuDetailsResponseWrapper { factory SkuDetailsResponseWrapper.fromJson(Map map) => _$SkuDetailsResponseWrapperFromJson(map); - /// The final status of the [BillingClient.querySkuDetails] call. - final BillingResponse responseCode; + /// The final result of the [BillingClient.querySkuDetails] call. + final BillingResultWrapper billingResult; /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails]. final List skuDetailsList; @@ -156,10 +168,48 @@ class SkuDetailsResponseWrapper { final SkuDetailsResponseWrapper typedOther = other; return typedOther is SkuDetailsResponseWrapper && - typedOther.responseCode == responseCode && + typedOther.billingResult == billingResult && typedOther.skuDetailsList == skuDetailsList; } @override - int get hashCode => hashValues(responseCode, skuDetailsList); + int get hashCode => hashValues(billingResult, skuDetailsList); +} + +/// Params containing the response code and the debug message from In-app Billing API response. +@JsonSerializable() +@BillingResponseConverter() +class BillingResultWrapper { + /// Constructs the object with [responseCode] and [debugMessage]. + BillingResultWrapper({@required this.responseCode, this.debugMessage}); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory BillingResultWrapper.fromJson(Map map) => + _$BillingResultWrapperFromJson(map); + + /// Response code returned in In-app Billing API calls. + final BillingResponse responseCode; + + /// Debug message returned in In-app Billing API calls. + /// + /// This message uses an en-US locale and should not be shown to users. + final String debugMessage; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + + final BillingResultWrapper typedOther = other; + return typedOther is BillingResultWrapper && + typedOther.responseCode == responseCode && + typedOther.debugMessage == debugMessage; + } + + @override + int get hashCode => hashValues(responseCode, debugMessage); } 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 3d059834a4f7..474ad0afae76 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 @@ -21,7 +21,9 @@ 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); + isRewarded: json['isRewarded'] as bool, + originalPrice: json['originalPrice'] as String, + originalPriceAmountMicros: json['originalPriceAmountMicros'] as int); } Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => @@ -39,13 +41,15 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => 'subscriptionPeriod': instance.subscriptionPeriod, 'title': instance.title, 'type': const SkuTypeConverter().toJson(instance.type), - 'isRewarded': instance.isRewarded + 'isRewarded': instance.isRewarded, + 'originalPrice': instance.originalPrice, + 'originalPriceAmountMicros': instance.originalPriceAmountMicros }; SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { return SkuDetailsResponseWrapper( - responseCode: const BillingResponseConverter() - .fromJson(json['responseCode'] as int), + billingResult: + BillingResultWrapper.fromJson(json['billingResult'] as Map), skuDetailsList: (json['skuDetailsList'] as List) .map((e) => SkuDetailsWrapper.fromJson(e as Map)) .toList()); @@ -53,8 +57,22 @@ SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { Map _$SkuDetailsResponseWrapperToJson( SkuDetailsResponseWrapper instance) => + { + 'billingResult': instance.billingResult, + 'skuDetailsList': instance.skuDetailsList + }; + +BillingResultWrapper _$BillingResultWrapperFromJson(Map json) { + return BillingResultWrapper( + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int), + debugMessage: json['debugMessage'] as String); +} + +Map _$BillingResultWrapperToJson( + BillingResultWrapper instance) => { 'responseCode': const BillingResponseConverter().toJson(instance.responseCode), - 'skuDetailsList': instance.skuDetailsList + 'debugMessage': instance.debugMessage }; 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 e6b8ac5e9e95..c287f674c891 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 @@ -62,13 +62,16 @@ class AppStoreConnection implements InAppPurchaseConnection { } @override - Future completePurchase(PurchaseDetails purchase) { - return _skPaymentQueueWrapper + Future completePurchase(PurchaseDetails purchase, + {String developerPayload}) async { + await _skPaymentQueueWrapper .finishTransaction(purchase.skPaymentTransaction); + return BillingResultWrapper(responseCode: BillingResponse.ok); } @override - Future consumePurchase(PurchaseDetails purchase) { + Future consumePurchase(PurchaseDetails purchase, + {String developerPayload}) { 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 d64467ed3775..a7a13b0dfe04 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 @@ -6,8 +6,12 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; import '../../billing_client_wrappers.dart'; +import '../../billing_client_wrappers.dart'; +import '../../billing_client_wrappers.dart'; +import '../../billing_client_wrappers.dart'; import 'in_app_purchase_connection.dart'; import 'product_details.dart'; @@ -50,10 +54,11 @@ class GooglePlayConnection @override Future buyNonConsumable({@required PurchaseParam purchaseParam}) async { - BillingResponse response = await billingClient.launchBillingFlow( - sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName); - return response == BillingResponse.ok; + BillingResultWrapper billingResultWrapper = + await billingClient.launchBillingFlow( + sku: purchaseParam.productDetails.id, + accountId: purchaseParam.applicationUserName); + return billingResultWrapper.responseCode == BillingResponse.ok; } @override @@ -66,14 +71,21 @@ class GooglePlayConnection } @override - Future completePurchase(PurchaseDetails purchase) { - throw UnsupportedError('complete purchase is not available on Android'); + Future completePurchase(PurchaseDetails purchase, + {String developerPayload}) { + AcknowledgeParams params = AcknowledgeParams( + purchaseToken: purchase.verificationData.serverVerificationData, + developerPayload: developerPayload); + return billingClient.acknowledgePurchase(params); } @override - Future consumePurchase(PurchaseDetails purchase) { - return billingClient - .consumeAsync(purchase.verificationData.serverVerificationData); + Future consumePurchase(PurchaseDetails purchase, + {String developerPayload}) { + ConsumeParams params = ConsumeParams( + purchaseToken: purchase.verificationData.serverVerificationData, + developerPayload: developerPayload); + return billingClient.consumeAsync(params); } @override @@ -90,9 +102,21 @@ class GooglePlayConnection exception = e; responses = [ PurchasesResultWrapper( - responseCode: BillingResponse.error, purchasesList: []), + responseCode: BillingResponse.error, + purchasesList: [], + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: e.details.toString(), + ), + ), PurchasesResultWrapper( - responseCode: BillingResponse.error, purchasesList: []) + responseCode: BillingResponse.error, + purchasesList: [], + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: e.details.toString(), + ), + ) ]; } @@ -188,10 +212,14 @@ class GooglePlayConnection responses = [ // ignore: invalid_use_of_visible_for_testing_member SkuDetailsResponseWrapper( - responseCode: BillingResponse.error, skuDetailsList: []), + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, debugMessage: e.code), + skuDetailsList: []), // ignore: invalid_use_of_visible_for_testing_member SkuDetailsResponseWrapper( - responseCode: BillingResponse.error, skuDetailsList: []) + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, debugMessage: e.code), + skuDetailsList: []) ]; } List productDetailsList = @@ -220,22 +248,18 @@ class GooglePlayConnection static Future> _getPurchaseDetailsFromResult( PurchasesResultWrapper resultWrapper) async { IAPError error; - PurchaseStatus status; - if (resultWrapper.responseCode == BillingResponse.ok) { - error = null; - status = PurchaseStatus.purchased; - } else { + if (resultWrapper.responseCode != BillingResponse.ok) { error = IAPError( source: IAPSource.GooglePlay, code: kPurchaseErrorCode, message: resultWrapper.responseCode.toString(), + details: resultWrapper.billingResult.debugMessage, ); - status = PurchaseStatus.error; } final List> purchases = resultWrapper.purchasesList.map((PurchaseWrapper purchase) { return _maybeAutoConsumePurchase(PurchaseDetails.fromPurchase(purchase) - ..status = status + ..status = _buildPurchaseStatus(purchase.purchaseState) ..error = error); }).toList(); if (!purchases.isEmpty) { @@ -260,18 +284,25 @@ class GooglePlayConnection return purchaseDetails; } - final BillingResponse consumedResponse = + final BillingResultWrapper billingResult = await instance.consumePurchase(purchaseDetails); + final BillingResponse consumedResponse = billingResult.responseCode; if (consumedResponse != BillingResponse.ok) { - purchaseDetails.status = PurchaseStatus.error; purchaseDetails.error = IAPError( source: IAPSource.GooglePlay, code: kConsumptionFailedErrorCode, message: consumedResponse.toString(), + details: billingResult.debugMessage, ); } + purchaseDetails.status = _buildPurchaseStatus( + purchaseDetails.billingClientPurchase.purchaseState); _productIdsToConsume.remove(purchaseDetails.productID); return purchaseDetails; } + + static PurchaseStatus _buildPurchaseStatus(PurchaseStateWrapper state) { + return PurchaseStateConverter().toPurchaseStatus(state); + } } 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 46088b9b008f..376ab97ed6c3 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 @@ -11,6 +11,8 @@ import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/billing_client_wrappers.dart'; import './purchase_details.dart'; +export 'package:in_app_purchase/billing_client_wrappers.dart'; + /// Basic API for making in app purchases across multiple platforms. /// /// This is a generic abstraction built from `billing_client_wrapers` and @@ -168,16 +170,23 @@ abstract class InAppPurchaseConnection { Future buyConsumable( {@required PurchaseParam purchaseParam, bool autoConsume = true}); - /// (App Store only) Mark that purchased content has been delivered to the + /// Mark that purchased content has been delivered to the /// user. /// /// You are responsible for completing every [PurchaseDetails] whose - /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or - /// [[PurchaseStatus.error]. Completing a [PurchaseStatus.pending] purchase - /// will cause an exception. - /// - /// This throws an [UnsupportedError] on Android. - Future completePurchase(PurchaseDetails purchase); + /// [PurchaseDetails.status] is [PurchaseStatus.purchased]. + /// Additionally, the purchase needs to be completed if the [PurchaseDetails.status] + /// [[PurchaseStatus.error]. + /// Completing a [PurchaseStatus.pending] purchase will cause an exception. + /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion. + /// + /// The method returns a [BillingResultWrapper] to indicate a detailed status of the complete process. + /// + /// Warning!Fail to call this method 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` only works on Android. + Future completePurchase(PurchaseDetails purchase, + {String developerPayload = null}); /// (Play only) Mark that the user has consumed a product. /// @@ -185,8 +194,11 @@ 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` can be specified to be associated with this consumption. + /// /// This throws an [UnsupportedError] on iOS. - Future consumePurchase(PurchaseDetails purchase); + Future consumePurchase(PurchaseDetails purchase, + {String developerPayload = null}); /// Query all previous purchases. /// diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index b980b01bfa46..c9bb10ac7aa8 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/purchase_wrapper.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; @@ -11,6 +12,8 @@ import './product_details.dart'; final String kPurchaseErrorCode = 'purchase_error'; final String kRestoredPurchaseErrorCode = 'restore_transactions_failed'; final String kConsumptionFailedErrorCode = 'consume_purchase_failed'; +final String _kPlatformIOS = 'ios'; +final String _kPlatformAndroid = 'android'; /// Represents the data that is used to verify purchases. /// @@ -122,7 +125,23 @@ class PurchaseDetails { final String transactionDate; /// The status that this [PurchaseDetails] is currently on. - PurchaseStatus status; + PurchaseStatus get status => _status; + set status(PurchaseStatus status) { + if (_platform == _kPlatformIOS) { + if (status == PurchaseStatus.purchased || + status == PurchaseStatus.error) { + pendingCompletePurchase = true; + } + } + if (_platform == _kPlatformAndroid) { + if (status == PurchaseStatus.purchased) { + pendingCompletePurchase = true; + } + } + _status = status; + } + + PurchaseStatus _status; /// The error is only available when [status] is [PurchaseStatus.error]. IAPError error; @@ -137,6 +156,17 @@ class PurchaseDetails { /// This is null on Android. final PurchaseWrapper billingClientPurchase; + /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true`. + /// + /// The initial value is `false`. + /// * See also [InAppPurchaseConnection.completePurchase] for more details on completing purchases. + bool pendingCompletePurchase = false; + + // The platform that the object is created on. + // + // The value is either '_kPlatformIOS' or '_kPlatformAndroid'. + String _platform; + PurchaseDetails({ @required this.purchaseID, @required this.productID, @@ -159,7 +189,10 @@ class PurchaseDetails { ? (transaction.transactionTimeStamp * 1000).toInt().toString() : null, this.skPaymentTransaction = transaction, - this.billingClientPurchase = null; + this.billingClientPurchase = null { + this.status = PurchaseStatus.pending; + _platform = _kPlatformIOS; + } /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. PurchaseDetails.fromPurchase(PurchaseWrapper purchase) @@ -171,7 +204,10 @@ class PurchaseDetails { source: IAPSource.GooglePlay), this.transactionDate = purchase.purchaseTime.toString(), this.skPaymentTransaction = null, - this.billingClientPurchase = purchase; + this.billingClientPurchase = purchase { + this.status = PurchaseStatus.pending; + _platform = _kPlatformAndroid; + } } /// The response object for fetching the past 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 818250607ed7..4044dac9e4eb 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 @@ -39,22 +39,37 @@ void main() { }); group('startConnection', () { - test('returns BillingResponse', () async { + final String methodName = + 'BillingClient#startConnection(BillingClientStateListener)'; + test('returns BillingResultWrapper', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse( - name: 'BillingClient#startConnection(BillingClientStateListener)', - value: BillingResponseConverter().toJson(BillingResponse.ok)); + name: methodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + ); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); expect( await billingClient.startConnection( onBillingServiceDisconnected: () {}), - equals(BillingResponse.ok)); + equals(billingResult)); }); test('passes handle to onBillingServiceDisconnected', () async { - final String methodName = - 'BillingClient#startConnection(BillingClientStateListener)'; + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse( - name: methodName, - value: BillingResponseConverter().toJson(BillingResponse.ok)); + name: methodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + ); await billingClient.startConnection(onBillingServiceDisconnected: () {}); final MethodCall call = stubPlatform.previousCallMatching(methodName); expect(call.arguments, equals({'handle': 0})); @@ -74,9 +89,13 @@ void main() { 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; test('handles empty skuDetails', () async { + const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse(name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, 'skuDetailsList': >[] }); @@ -84,14 +103,20 @@ void main() { .querySkuDetails( skuType: SkuType.inapp, skusList: ['invalid']); - expect(response.responseCode, equals(responseCode)); + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + expect(response.billingResult, equals(billingResult)); expect(response.skuDetailsList, isEmpty); }); test('returns SkuDetailsResponseWrapper', () async { + const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.ok; stubPlatform.addResponse(name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] }); @@ -99,7 +124,9 @@ void main() { .querySkuDetails( skuType: SkuType.inapp, skusList: ['invalid']); - expect(response.responseCode, equals(responseCode)); + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + expect(response.billingResult, equals(billingResult)); expect(response.skuDetailsList, contains(dummySkuDetails)); }); }); @@ -109,17 +136,21 @@ void main() { 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; test('serializes and deserializes data', () async { - final BillingResponse sentCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( - name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode)); + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; - final BillingResponse receivedCode = await billingClient - .launchBillingFlow(sku: skuDetails.sku, accountId: accountId); - - expect(receivedCode, equals(sentCode)); + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, accountId: accountId), + equals(expectedBillingResult)); Map arguments = stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); @@ -127,16 +158,18 @@ void main() { }); test('handles null accountId', () async { - final BillingResponse sentCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( - name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode)); + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); final SkuDetailsWrapper skuDetails = dummySkuDetails; - final BillingResponse receivedCode = - await billingClient.launchBillingFlow(sku: skuDetails.sku); - - expect(receivedCode, equals(sentCode)); + expect(await billingClient.launchBillingFlow(sku: skuDetails.sku), + equals(expectedBillingResult)); Map arguments = stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); @@ -153,8 +186,12 @@ void main() { final List expectedList = [ dummyPurchase ]; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform .addResponse(name: queryPurchasesMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(expectedCode), 'purchasesList': expectedList .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) @@ -164,6 +201,7 @@ void main() { final PurchasesResultWrapper response = await billingClient.queryPurchases(SkuType.inapp); + expect(response.billingResult, equals(expectedBillingResult)); expect(response.responseCode, equals(expectedCode)); expect(response.purchasesList, equals(expectedList)); }); @@ -174,8 +212,12 @@ void main() { test('handles empty purchases', () async { final BillingResponse expectedCode = BillingResponse.userCanceled; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform .addResponse(name: queryPurchasesMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(expectedCode), 'purchasesList': [], }); @@ -183,6 +225,7 @@ void main() { final PurchasesResultWrapper response = await billingClient.queryPurchases(SkuType.inapp); + expect(response.billingResult, equals(expectedBillingResult)); expect(response.responseCode, equals(expectedCode)); expect(response.purchasesList, isEmpty); }); @@ -194,23 +237,27 @@ void main() { test('serializes and deserializes data', () async { final BillingResponse expectedCode = BillingResponse.ok; - final List expectedList = [ - dummyPurchase + final List expectedList = + [ + dummyPurchaseHistoryRecord, ]; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: queryPurchaseHistoryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(expectedCode), - 'purchasesList': expectedList - .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchaseHistoryRecordList': expectedList + .map((PurchaseHistoryRecordWrapper purchaseHistoryRecord) => + buildPurchaseHistoryRecordMap(purchaseHistoryRecord)) .toList(), }); - final PurchasesResultWrapper response = + final PurchasesHistoryResult response = await billingClient.queryPurchaseHistory(SkuType.inapp); - - expect(response.responseCode, equals(expectedCode)); - expect(response.purchasesList, equals(expectedList)); + expect(response.billingResult, equals(expectedBillingResult)); + expect(response.purchaseHistoryRecordList, equals(expectedList)); }); test('checks for null params', () async { @@ -220,18 +267,19 @@ void main() { test('handles empty purchases', () async { final BillingResponse expectedCode = BillingResponse.userCanceled; - stubPlatform.addResponse( - name: queryPurchaseHistoryMethodName, - value: { - 'responseCode': BillingResponseConverter().toJson(expectedCode), - 'purchasesList': [], - }); + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryPurchaseHistoryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchaseHistoryRecordList': [], + }); - final PurchasesResultWrapper response = + final PurchasesHistoryResult response = await billingClient.queryPurchaseHistory(SkuType.inapp); - expect(response.responseCode, equals(expectedCode)); - expect(response.purchasesList, isEmpty); + expect(response.billingResult, equals(expectedBillingResult)); + expect(response.purchaseHistoryRecordList, isEmpty); }); }); @@ -240,14 +288,42 @@ void main() { 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; test('consume purchase async success', () async { final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, - value: BillingResponseConverter().toJson(expectedCode)); + value: buildBillingResultMap(expectedBillingResult)); + + final ConsumeParams params = ConsumeParams( + purchaseToken: 'dummy token', developerPayload: 'dummy payload'); + + final BillingResultWrapper billingResult = + await billingClient.consumeAsync(params); + + expect(billingResult, equals(expectedBillingResult)); + }); + }); + + group('acknowledge purchases', () { + const String acknowledgeMethodName = + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + test('acknowledge purchase success', () async { + final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: acknowledgeMethodName, + value: buildBillingResultMap(expectedBillingResult)); + + final AcknowledgeParams params = AcknowledgeParams( + purchaseToken: 'dummy token', developerPayload: 'dummy payload'); - final BillingResponse responseCode = - await billingClient.consumeAsync('dummy token'); + final BillingResultWrapper billingResult = + await billingClient.acknowledgePurchase(params); - expect(responseCode, equals(expectedCode)); + expect(billingResult, equals(expectedBillingResult)); }); }); } diff --git a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart index f1865b41842f..aad5ebd5eec2 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -17,6 +17,19 @@ final PurchaseWrapper dummyPurchase = PurchaseWrapper( purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', + developerPayload: 'dummy payload', + isAcknowledged: true, + purchaseState: PurchaseStateWrapper.purchased, +); + +final PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = + PurchaseHistoryRecordWrapper( + purchaseTime: 0, + signature: 'signature', + sku: 'sku', + purchaseToken: 'purchaseToken', + originalJson: '', + developerPayload: 'dummy payload', ); void main() { @@ -45,6 +58,17 @@ void main() { }); }); + group('PurchaseHistoryRecordWrapper', () { + test('converts from map', () { + final PurchaseHistoryRecordWrapper expected = dummyPurchaseHistoryRecord; + final PurchaseHistoryRecordWrapper parsed = + PurchaseHistoryRecordWrapper.fromJson( + buildPurchaseHistoryRecordMap(expected)); + + expect(parsed, equals(expected)); + }); + }); + group('PurchasesResultWrapper', () { test('parsed from map', () { final BillingResponse responseCode = BillingResponse.ok; @@ -52,22 +76,55 @@ void main() { dummyPurchase, dummyPurchase ]; + const String debugMessage = 'dummy Message'; + final BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); final PurchasesResultWrapper expected = PurchasesResultWrapper( - responseCode: responseCode, purchasesList: purchases); - + billingResult: billingResult, + responseCode: responseCode, + purchasesList: purchases); final PurchasesResultWrapper parsed = PurchasesResultWrapper.fromJson({ + 'billingResult': buildBillingResultMap(billingResult), 'responseCode': BillingResponseConverter().toJson(responseCode), 'purchasesList': >[ buildPurchaseMap(dummyPurchase), buildPurchaseMap(dummyPurchase) ] }); - + expect(parsed.billingResult, equals(expected.billingResult)); expect(parsed.responseCode, equals(expected.responseCode)); expect(parsed.purchasesList, containsAll(expected.purchasesList)); }); }); + + group('PurchasesHistoryResult', () { + test('parsed from map', () { + final BillingResponse responseCode = BillingResponse.ok; + final List purchaseHistoryRecordList = + [ + dummyPurchaseHistoryRecord, + dummyPurchaseHistoryRecord + ]; + const String debugMessage = 'dummy Message'; + final BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final PurchasesHistoryResult expected = PurchasesHistoryResult( + billingResult: billingResult, + purchaseHistoryRecordList: purchaseHistoryRecordList); + final PurchasesHistoryResult parsed = + PurchasesHistoryResult.fromJson({ + 'billingResult': buildBillingResultMap(billingResult), + 'purchaseHistoryRecordList': >[ + buildPurchaseHistoryRecordMap(dummyPurchaseHistoryRecord), + buildPurchaseHistoryRecordMap(dummyPurchaseHistoryRecord) + ] + }); + expect(parsed.billingResult, equals(billingResult)); + expect(parsed.purchaseHistoryRecordList, + containsAll(expected.purchaseHistoryRecordList)); + }); + }); } Map buildPurchaseMap(PurchaseWrapper original) { @@ -80,5 +137,27 @@ Map buildPurchaseMap(PurchaseWrapper original) { 'purchaseToken': original.purchaseToken, 'isAutoRenewing': original.isAutoRenewing, 'originalJson': original.originalJson, + 'developerPayload': original.developerPayload, + 'purchaseState': PurchaseStateConverter().toJson(original.purchaseState), + 'isAcknowledged': original.isAcknowledged, + }; +} + +Map buildPurchaseHistoryRecordMap( + PurchaseHistoryRecordWrapper original) { + return { + 'purchaseTime': original.purchaseTime, + 'signature': original.signature, + 'sku': original.sku, + 'purchaseToken': original.purchaseToken, + 'originalJson': original.originalJson, + 'developerPayload': original.developerPayload, + }; +} + +Map buildBillingResultMap(BillingResultWrapper original) { + return { + 'responseCode': BillingResponseConverter().toJson(original.responseCode), + 'debugMessage': original.debugMessage, }; } 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 ace2f41b886a..c305e6df88cc 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 @@ -4,8 +4,8 @@ import 'package:test/test.dart'; import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; +import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( description: 'description', @@ -22,6 +22,8 @@ final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( title: 'title', type: SkuType.inapp, isRewarded: true, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, ); void main() { @@ -38,23 +40,29 @@ void main() { group('SkuDetailsResponseWrapper', () { test('parsed from map', () { final BillingResponse responseCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; final List skusDetails = [ dummySkuDetails, dummySkuDetails ]; + BillingResultWrapper result = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( - responseCode: responseCode, skuDetailsList: skusDetails); + billingResult: result, skuDetailsList: skusDetails); final SkuDetailsResponseWrapper parsed = SkuDetailsResponseWrapper.fromJson({ - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, 'skuDetailsList': >[ buildSkuMap(dummySkuDetails), buildSkuMap(dummySkuDetails) ] }); - expect(parsed.responseCode, equals(expected.responseCode)); + expect(parsed.billingResult, equals(expected.billingResult)); expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); }); @@ -72,17 +80,23 @@ void main() { test('handles empty list of skuDetails', () { final BillingResponse responseCode = BillingResponse.error; + const String debugMessage = 'dummy message'; final List skusDetails = []; + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( - responseCode: responseCode, skuDetailsList: skusDetails); + billingResult: billingResult, skuDetailsList: skusDetails); final SkuDetailsResponseWrapper parsed = SkuDetailsResponseWrapper.fromJson({ - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, 'skuDetailsList': >[] }); - expect(parsed.responseCode, equals(expected.responseCode)); + expect(parsed.billingResult, equals(expected.billingResult)); expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); }); }); @@ -104,5 +118,7 @@ Map buildSkuMap(SkuDetailsWrapper original) { '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/app_store_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart index ae24167b6a7c..9f963c4c99b7 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart @@ -245,7 +245,7 @@ void main() { subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); purchaseDetailsList.forEach((purchaseDetails) { - if (purchaseDetails.status == PurchaseStatus.purchased) { + if (purchaseDetails.pendingCompletePurchase) { AppStoreConnection.instance.completePurchase(purchaseDetails); completer.complete(details); subscription.cancel(); 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 512664a24af0..94714f63b31e 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 @@ -35,9 +35,13 @@ void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: startConnectionCall, - value: BillingResponseConverter().toJson(BillingResponse.ok)); + value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall, value: null); connection = GooglePlayConnection.instance; }); @@ -82,10 +86,13 @@ void main() { 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; test('handles empty skuDetails', () async { - final BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse(name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'skuDetailsList': >[] + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'skuDetailsList': [], }); final ProductDetailsResponse response = @@ -94,9 +101,12 @@ void main() { }); test('should get correct product details', () async { + const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': buildBillingResultMap(expectedBillingResult), 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] }); // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead @@ -110,9 +120,12 @@ void main() { }); test('should get the correct notFoundIDs', () async { + const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': buildBillingResultMap(expectedBillingResult), 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] }); // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead @@ -157,8 +170,13 @@ void main() { group('queryPurchaseDetails', () { const String queryMethodName = 'BillingClient#queryPurchases(String)'; test('handles error', () async { + const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(responseCode), 'purchasesList': >[] }); @@ -170,8 +188,13 @@ void main() { }); test('returns SkuDetailsResponseWrapper', () async { + const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(responseCode), 'purchasesList': >[ buildPurchaseMap(dummyPurchase), @@ -187,11 +210,16 @@ void main() { }); test('should store platform exception in the response', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: queryMethodName, value: { 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': buildBillingResultMap(expectedBillingResult), 'purchasesList': >[] }, additionalStepBeforeReturn: (_) { @@ -225,13 +253,18 @@ void main() { test('buy non consumable, serializes and deserializes data', () async { final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode), + value: buildBillingResultMap(expectedBillingResult), additionalStepBeforeReturn: (_) { // Mock java update purchase callback. MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(sentCode), 'purchasesList': [ { @@ -242,7 +275,10 @@ void main() { 'purchaseTime': 1231231231, 'purchaseToken': "token", 'signature': 'sign', - 'originalJson': 'json' + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, } ] }); @@ -274,13 +310,18 @@ void main() { test('handles an error with an empty purchases list', () async { final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; final BillingResponse sentCode = BillingResponse.error; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode), + value: buildBillingResultMap(expectedBillingResult), additionalStepBeforeReturn: (_) { // Mock java update purchase callback. MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(sentCode), 'purchasesList': [] }); @@ -313,13 +354,18 @@ void main() { () async { final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode), + value: buildBillingResultMap(expectedBillingResult), additionalStepBeforeReturn: (_) { // Mock java update purchase callback. MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(sentCode), 'purchasesList': [ { @@ -330,7 +376,10 @@ void main() { 'purchaseTime': 1231231231, 'purchaseToken': "token", 'signature': 'sign', - 'originalJson': 'json' + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, } ] }); @@ -339,9 +388,12 @@ void main() { Completer consumeCompleter = Completer(); // adding call back for consume purchase final BillingResponse expectedCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, - value: BillingResponseConverter().toJson(expectedCode), + value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { String purchaseToken = args['purchaseToken']; consumeCompleter.complete((purchaseToken)); @@ -374,10 +426,13 @@ void main() { test('buyNonConsumable propagates failures to launch the billing flow', () async { + const String debugMessage = 'dummy message'; final BillingResponse sentCode = BillingResponse.error; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode)); + value: buildBillingResultMap(expectedBillingResult)); final bool result = await GooglePlayConnection.instance.buyNonConsumable( purchaseParam: PurchaseParam( @@ -389,10 +444,14 @@ void main() { test('buyConsumable propagates failures to launch the billing flow', () async { - final BillingResponse sentCode = BillingResponse.error; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( - name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode)); + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); final bool result = await GooglePlayConnection.instance.buyConsumable( purchaseParam: PurchaseParam( @@ -405,13 +464,17 @@ void main() { test('adds consumption failures to PurchaseDetails objects', () async { final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode), + value: buildBillingResultMap(expectedBillingResult), additionalStepBeforeReturn: (_) { // Mock java update purchase callback. MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(sentCode), 'purchasesList': [ { @@ -422,7 +485,10 @@ void main() { 'purchaseTime': 1231231231, 'purchaseToken': "token", 'signature': 'sign', - 'originalJson': 'json' + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, } ] }); @@ -431,9 +497,12 @@ void main() { Completer consumeCompleter = Completer(); // adding call back for consume purchase final BillingResponse expectedCode = BillingResponse.error; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, - value: BillingResponseConverter().toJson(expectedCode), + value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { String purchaseToken = args['purchaseToken']; consumeCompleter.complete((purchaseToken)); @@ -469,13 +538,18 @@ void main() { () async { final SkuDetailsWrapper skuDetails = dummySkuDetails; final String accountId = "hashedAccountId"; - final BillingResponse sentCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( name: launchMethodName, - value: BillingResponseConverter().toJson(sentCode), + value: buildBillingResultMap(expectedBillingResult), additionalStepBeforeReturn: (_) { // Mock java update purchase callback. MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), 'responseCode': BillingResponseConverter().toJson(sentCode), 'purchasesList': [ { @@ -486,7 +560,10 @@ void main() { 'purchaseTime': 1231231231, 'purchaseToken': "token", 'signature': 'sign', - 'originalJson': 'json' + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, } ] }); @@ -495,9 +572,12 @@ void main() { Completer consumeCompleter = Completer(); // adding call back for consume purchase final BillingResponse expectedCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, - value: BillingResponseConverter().toJson(expectedCode), + value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { String purchaseToken = args['purchaseToken']; consumeCompleter.complete((purchaseToken)); @@ -524,20 +604,46 @@ void main() { 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; test('consume purchase async success', () async { final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( - name: consumeMethodName, - value: BillingResponseConverter().toJson(expectedCode)); - - final BillingResponse responseCode = await GooglePlayConnection.instance - .consumePurchase(PurchaseDetails.fromPurchase(dummyPurchase)); - - expect(responseCode, equals(expectedCode)); + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final BillingResultWrapper billingResultWrapper = + await GooglePlayConnection.instance + .consumePurchase(PurchaseDetails.fromPurchase(dummyPurchase)); + + expect(billingResultWrapper, equals(expectedBillingResult)); }); }); group('complete purchase', () { - test('calling complete purchase on android should throw', () async { - expect(() => connection.completePurchase(null), throwsUnsupportedError); + const String consumeMethodName = + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + test('complete purchase success', () async { + final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + PurchaseDetails purchaseDetails = + PurchaseDetails.fromPurchase(dummyPurchase); + Completer completer = Completer(); + purchaseDetails.status = PurchaseStatus.purchased; + if (purchaseDetails.pendingCompletePurchase) { + final BillingResultWrapper billingResultWrapper = + await GooglePlayConnection.instance.completePurchase( + purchaseDetails, + developerPayload: 'dummy payload'); + expect(billingResultWrapper, equals(expectedBillingResult)); + completer.complete(billingResultWrapper); + } + expect(await completer.future, equals(expectedBillingResult)); }); }); } From d47ae9cd37ad8be3350eaadc59c0979e5c977269 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Nov 2019 13:51:32 -0800 Subject: [PATCH 02/16] fix ci --- .../src/in_app_purchase/google_play_connection.dart | 11 +++-------- .../lib/src/in_app_purchase/purchase_details.dart | 9 ++++++--- .../google_play_connection_test.dart | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) 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 a7a13b0dfe04..6c1d462dceb8 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 @@ -9,9 +9,6 @@ import 'package:flutter/widgets.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; import '../../billing_client_wrappers.dart'; -import '../../billing_client_wrappers.dart'; -import '../../billing_client_wrappers.dart'; -import '../../billing_client_wrappers.dart'; import 'in_app_purchase_connection.dart'; import 'product_details.dart'; @@ -258,9 +255,8 @@ class GooglePlayConnection } final List> purchases = resultWrapper.purchasesList.map((PurchaseWrapper purchase) { - return _maybeAutoConsumePurchase(PurchaseDetails.fromPurchase(purchase) - ..status = _buildPurchaseStatus(purchase.purchaseState) - ..error = error); + return _maybeAutoConsumePurchase( + PurchaseDetails.fromPurchase(purchase)..error = error); }).toList(); if (!purchases.isEmpty) { return Future.wait(purchases); @@ -288,6 +284,7 @@ class GooglePlayConnection await instance.consumePurchase(purchaseDetails); final BillingResponse consumedResponse = billingResult.responseCode; if (consumedResponse != BillingResponse.ok) { + purchaseDetails.status = PurchaseStatus.error; purchaseDetails.error = IAPError( source: IAPSource.GooglePlay, code: kConsumptionFailedErrorCode, @@ -295,8 +292,6 @@ class GooglePlayConnection details: billingResult.debugMessage, ); } - purchaseDetails.status = _buildPurchaseStatus( - purchaseDetails.billingClientPurchase.purchaseState); _productIdsToConsume.remove(purchaseDetails.productID); return purchaseDetails; diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index c9bb10ac7aa8..ef927a78ab45 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/purchase_wrapper.dart'; +import 'package:in_app_purchase/src/store_kit_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; import './in_app_purchase_connection.dart'; import './product_details.dart'; @@ -190,7 +191,8 @@ class PurchaseDetails { : null, this.skPaymentTransaction = transaction, this.billingClientPurchase = null { - this.status = PurchaseStatus.pending; + this.status = SKTransactionStatusConverter() + .toPurchaseStatus(transaction.transactionState); _platform = _kPlatformIOS; } @@ -205,7 +207,8 @@ class PurchaseDetails { this.transactionDate = purchase.purchaseTime.toString(), this.skPaymentTransaction = null, this.billingClientPurchase = purchase { - this.status = PurchaseStatus.pending; + this.status = + PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState); _platform = _kPlatformAndroid; } } 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 94714f63b31e..e6f4de06a2fd 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 @@ -505,7 +505,7 @@ void main() { value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { String purchaseToken = args['purchaseToken']; - consumeCompleter.complete((purchaseToken)); + consumeCompleter.complete(purchaseToken); }); Completer completer = Completer(); From a10b0ea33f8d107b5e830a147f4dd73d89246e47 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 25 Nov 2019 09:59:42 -0800 Subject: [PATCH 03/16] review fixes --- packages/in_app_purchase/CHANGELOG.md | 2 +- packages/in_app_purchase/README.md | 6 +- .../inapppurchase/MethodCallHandlerTest.java | 2 +- .../plugins/inapppurchase/TranslatorTest.java | 10 ++++ .../in_app_purchase/example/lib/main.dart | 2 + .../billing_client_wrapper.dart | 27 +++++---- .../enum_converters.dart | 15 ++--- .../purchase_wrapper.dart | 60 +++++++------------ .../sku_details_wrapper.dart | 6 +- .../google_play_connection.dart | 10 ++-- .../in_app_purchase_connection.dart | 8 +-- .../src/in_app_purchase/purchase_details.dart | 22 +++---- packages/in_app_purchase/pubspec.yaml | 2 +- .../billing_client_wrapper_test.dart | 13 ++-- 14 files changed, 86 insertions(+), 99 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 9e65fbb582a5..03cb6d5a9365 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.2 +## 0.3.0 * Migrate the `Google Play Library` to 2.0.3. * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md index ce564d14fea3..c366e149bd8d 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -114,15 +114,15 @@ for (PurchaseDetails purchase in response.pastPurchases) { } ``` -Note that the App Store does not have any APIs for querying consummable -products, and Google Play considers consummable products to no longer be owned +Note that the App Store does not have any APIs for querying consumable +products, and Google Play considers consumable products to no longer be owned once they're marked as consumed and fails to return them here. For restoring these across devices you'll need to persist them on your own server and query that as well. ### Making a purchase -Both storefronts handle consummable and non-consummable products differently. If +Both storefronts handle consumable and non-consumable products differently. If you're using `InAppPurchaseConnection`, you need to make a distinction here and call the right purchase method for each type. 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 f032c9d89580..14d0e560e0b0 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 @@ -513,7 +513,7 @@ public void consumeAsync() { } @Test - public void acknowledgetPurchase() { + public void acknowledgePurchase() { establishConnectedBillingClient(null, null); ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); BillingResult billingResult = diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 5b5ab4ff14f7..319b34ce787f 100644 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -158,6 +158,16 @@ public void fromBillingResult() throws JSONException { assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); } + @Test + public void fromBillingResult_dubugMessageNull() throws JSONException { + BillingResult newBillingResult = + BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build(); + Map billingResultMap = Translator.fromBillingResult(newBillingResult); + + assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); + assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); + } + private void assertSerialized(SkuDetails expected, Map serialized) { assertEquals(expected.getDescription(), serialized.get("description")); assertEquals(expected.getFreeTrialPeriod(), serialized.get("freeTrialPeriod")); diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 025cc9513255..2fe27fcb8fdf 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -366,12 +366,14 @@ class _MyAppState extends State { } else { if (purchaseDetails.status == PurchaseStatus.error) { handleError(purchaseDetails.error); + return; } else if (purchaseDetails.status == PurchaseStatus.purchased) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { deliverProduct(purchaseDetails); } else { _handleInvalidPurchase(purchaseDetails); + return; } } if (Platform.isAndroid) { 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 3daaeaf0e60c..2ca4dcd1dd51 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 @@ -206,21 +206,23 @@ 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 `params` must not be null. + /// The `purchaseToken` must not be null. + /// 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(ConsumeParams params) async { - assert(params != null); + Future consumeAsync(String purchaseToken, + {String developerPayload}) async { + assert(purchaseToken != null); return BillingResultWrapper.fromJson(await channel .invokeMapMethod( 'BillingClient#consumeAsync(String, ConsumeResponseListener)', { - 'purchaseToken': params.purchaseToken, - 'developerPayload': params.developerPayload, + 'purchaseToken': purchaseToken, + 'developerPayload': developerPayload, })); } - /// Acknowledge an In-App purchase. + /// Acknowledge an in-app purchase. /// /// The developer is required to acknowledge that they have granted entitlement for all in-app purchases. /// @@ -235,18 +237,19 @@ class BillingClient { /// Please refer to https://developer.android.com/google/play/billing/billing_library_overview#acknowledge for more /// details. /// - /// The `params` must not be null. + /// The `purchaseToken` must not be null. + /// 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( - AcknowledgeParams params) async { - assert(params != null); + Future acknowledgePurchase(String purchaseToken, + {String developerPayload}) async { + assert(purchaseToken != null); return BillingResultWrapper.fromJson(await channel.invokeMapMethod( 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', { - 'purchaseToken': params.purchaseToken, - 'developerPayload': params.developerPayload, + 'purchaseToken': purchaseToken, + 'developerPayload': developerPayload, })); } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart index 1e81895438c3..864f24c98e55 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart @@ -9,9 +9,8 @@ import 'package:json_annotation/json_annotation.dart'; part 'enum_converters.g.dart'; /// Serializer for [BillingResponse]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@BillingResponseConverter()`. +// Use these in `@JsonSerializable()` classes by annotating them with +// `@BillingResponseConverter()`. class BillingResponseConverter implements JsonConverter { const BillingResponseConverter(); @@ -24,9 +23,8 @@ class BillingResponseConverter implements JsonConverter { } /// Serializer for [SkuType]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SkuTypeConverter()`. +// Use these in `@JsonSerializable()` classes by annotating them with +// `@SkuTypeConverter()`. class SkuTypeConverter implements JsonConverter { const SkuTypeConverter(); @@ -47,9 +45,8 @@ class _SerializedEnums { } /// Serializer for [PurchaseStateWrapper]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@PurchaseStateConverter()`. +// Use these in `@JsonSerializable()` classes by annotating them with +// `@PurchaseStateConverter()`. class PurchaseStateConverter implements JsonConverter { const PurchaseStateConverter(); 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 af191ce47fde..234b9dc0909d 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 @@ -109,9 +109,15 @@ class PurchaseWrapper { final String developerPayload; /// Whether the purchase has been acknowledged. + /// + /// A successful purchase has to be acknowledged within 3 days after the purchase via [BillingClient.acknowledgePurchase]. + /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. final bool isAcknowledged; - /// The state of purchase. + /// Determines the current state of the purchase. + /// + /// [BillingClient.acknowledgePurchase] should only be called when the `purchaseState` is [PurchaseStateWrapper.purchased]. + /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. final PurchaseStateWrapper purchaseState; } @@ -264,57 +270,31 @@ class PurchasesHistoryResult { final List purchaseHistoryRecordList; } -/// The parameter object used when consuming a purchase. -/// -/// See also [BillingClient.consumeAsync] for consuming a purchase. -class ConsumeParams { - /// Constructs the [ConsumeParams]. - /// - /// The `purchaseToken` must not be null. - /// The default value of `developerPayload` is null. - ConsumeParams({@required this.purchaseToken, this.developerPayload = null}); - - /// The developer data associated with the purchase to be consumed. - /// - /// Defaults to null. - final String developerPayload; - - /// The token that identifies the purchase to be consumed. - final String purchaseToken; -} - -/// The parameter object used when acknowledge a purchase. -/// -/// See also [BillingClient.acknowledgePurchase] for acknowledging a purchase. -class AcknowledgeParams { - /// Constructs the [AcknowledgeParams]. - /// - /// The `purchaseToken` must not be null. - /// The default value of `developerPayload` is null. - AcknowledgeParams( - {@required this.purchaseToken, this.developerPayload = null}); - - /// The developer data associated with the purchase to be acknowledged. - /// - /// Defaults to null. - final String developerPayload; - - /// The token that identifies the purchase to be acknowledged. - final String purchaseToken; -} - /// Possible state of a [PurchaseWrapper]. /// /// Wraps /// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html). /// * See also: [PurchaseWrapper]. enum PurchaseStateWrapper { + /// The state is unspecified. + /// + /// No actions on the [PurchaseWrapper] should be performed on this state. @JsonValue(0) unspecified_state, + /// The state is purchased. + /// + /// The production should be delivered and the purchase should be acknowledged on this state. + /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. @JsonValue(1) purchased, + /// The state is pending. + /// + /// No actions on the [PurchaseWrapper] should be performed on this state. + /// Wait for the state becomes [PurchaseStateWrapper.purchased] to perform further actions. + /// + /// A UI can be also displayed on this state to indicate the purchase is in action. @JsonValue(2) pending, } 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 4bce2e5e9f37..4d6a9307a53a 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 @@ -176,7 +176,7 @@ class SkuDetailsResponseWrapper { int get hashCode => hashValues(billingResult, skuDetailsList); } -/// Params containing the response code and the debug message from In-app Billing API response. +/// Params containing the response code and the debug message from the Play Billing API response. @JsonSerializable() @BillingResponseConverter() class BillingResultWrapper { @@ -190,10 +190,10 @@ class BillingResultWrapper { factory BillingResultWrapper.fromJson(Map map) => _$BillingResultWrapperFromJson(map); - /// Response code returned in In-app Billing API calls. + /// Response code returned in the Play Billing API calls. final BillingResponse responseCode; - /// Debug message returned in In-app Billing API calls. + /// Debug message returned in the Play Billing API calls. /// /// This message uses an en-US locale and should not be shown to users. final String debugMessage; 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 6c1d462dceb8..3be369885e4b 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 @@ -70,19 +70,17 @@ class GooglePlayConnection @override Future completePurchase(PurchaseDetails purchase, {String developerPayload}) { - AcknowledgeParams params = AcknowledgeParams( - purchaseToken: purchase.verificationData.serverVerificationData, + return billingClient.acknowledgePurchase( + purchase.verificationData.serverVerificationData, developerPayload: developerPayload); - return billingClient.acknowledgePurchase(params); } @override Future consumePurchase(PurchaseDetails purchase, {String developerPayload}) { - ConsumeParams params = ConsumeParams( - purchaseToken: purchase.verificationData.serverVerificationData, + return billingClient.consumeAsync( + purchase.verificationData.serverVerificationData, developerPayload: developerPayload); - return billingClient.consumeAsync(params); } @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 376ab97ed6c3..7d45d165af9d 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 @@ -174,16 +174,16 @@ abstract class InAppPurchaseConnection { /// user. /// /// You are responsible for completing every [PurchaseDetails] whose - /// [PurchaseDetails.status] is [PurchaseStatus.purchased]. - /// Additionally, the purchase needs to be completed if the [PurchaseDetails.status] - /// [[PurchaseStatus.error]. + /// [PurchaseDetails.status] is [PurchaseStatus.purchased]. Additionally on iOS, + /// the purchase needs to be completed if the [PurchaseDetails.status] is [PurchaseStatus.error]. /// Completing a [PurchaseStatus.pending] purchase will cause an exception. /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion. /// /// The method returns a [BillingResultWrapper] to indicate a detailed status of the complete process. /// - /// Warning!Fail to call this method within 3 days of the purchase will result a refund on Android. + /// Warning!Failure to call this method 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` only works on Android. Future completePurchase(PurchaseDetails purchase, {String developerPayload = null}); diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index ef927a78ab45..c8dd13df9768 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -131,12 +131,12 @@ class PurchaseDetails { if (_platform == _kPlatformIOS) { if (status == PurchaseStatus.purchased || status == PurchaseStatus.error) { - pendingCompletePurchase = true; + _pendingCompletePurchase = true; } } if (_platform == _kPlatformAndroid) { if (status == PurchaseStatus.purchased) { - pendingCompletePurchase = true; + _pendingCompletePurchase = true; } } _status = status; @@ -157,11 +157,13 @@ class PurchaseDetails { /// This is null on Android. final PurchaseWrapper billingClientPurchase; - /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true`. + /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true` + /// and the product has been delivered to the user. /// /// The initial value is `false`. /// * See also [InAppPurchaseConnection.completePurchase] for more details on completing purchases. - bool pendingCompletePurchase = false; + bool get pendingCompletePurchase => _pendingCompletePurchase; + bool _pendingCompletePurchase = false; // The platform that the object is created on. // @@ -190,9 +192,9 @@ class PurchaseDetails { ? (transaction.transactionTimeStamp * 1000).toInt().toString() : null, this.skPaymentTransaction = transaction, - this.billingClientPurchase = null { - this.status = SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState); + this.billingClientPurchase = null, + _status = SKTransactionStatusConverter() + .toPurchaseStatus(transaction.transactionState) { _platform = _kPlatformIOS; } @@ -206,9 +208,9 @@ class PurchaseDetails { source: IAPSource.GooglePlay), this.transactionDate = purchase.purchaseTime.toString(), this.skPaymentTransaction = null, - this.billingClientPurchase = purchase { - this.status = - PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState); + this.billingClientPurchase = purchase, + _status = + PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState) { _platform = _kPlatformAndroid; } } diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index 73f0e4f8c3e2..d3e8538c3c9f 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.2.2+2 +version: 0.3.0 dependencies: 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 4044dac9e4eb..d847757ae26f 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 @@ -295,11 +295,8 @@ void main() { name: consumeMethodName, value: buildBillingResultMap(expectedBillingResult)); - final ConsumeParams params = ConsumeParams( - purchaseToken: 'dummy token', developerPayload: 'dummy payload'); - - final BillingResultWrapper billingResult = - await billingClient.consumeAsync(params); + final BillingResultWrapper billingResult = await billingClient + .consumeAsync('dummy token', developerPayload: 'dummy payload'); expect(billingResult, equals(expectedBillingResult)); }); @@ -317,11 +314,9 @@ void main() { name: acknowledgeMethodName, value: buildBillingResultMap(expectedBillingResult)); - final AcknowledgeParams params = AcknowledgeParams( - purchaseToken: 'dummy token', developerPayload: 'dummy payload'); - final BillingResultWrapper billingResult = - await billingClient.acknowledgePurchase(params); + await billingClient.acknowledgePurchase('dummy token', + developerPayload: 'dummy payload'); expect(billingResult, equals(expectedBillingResult)); }); From 7fec09a7209e0eb196c1401536dd727ff4b52c2f Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 25 Nov 2019 10:18:22 -0800 Subject: [PATCH 04/16] some cleanup --- .../billing_client_wrappers/purchase_wrapper.dart | 13 +++++++------ .../src/in_app_purchase/google_play_connection.dart | 7 +++++-- 2 files changed, 12 insertions(+), 8 deletions(-) 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 234b9dc0909d..118d7502fe47 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 @@ -282,19 +282,20 @@ enum PurchaseStateWrapper { @JsonValue(0) unspecified_state, - /// The state is purchased. + /// The user has completed the purchase process. /// - /// The production should be delivered and the purchase should be acknowledged on this state. + /// The production should be delivered and then the purchase should be acknowledged. /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. @JsonValue(1) purchased, - /// The state is pending. + /// The user has started the purchase process. /// - /// No actions on the [PurchaseWrapper] should be performed on this state. - /// Wait for the state becomes [PurchaseStateWrapper.purchased] to perform further actions. + /// The user should follow the instructions that were given to them by the Play + /// Billing Library to complete the purchase. /// - /// A UI can be also displayed on this state to indicate the purchase is in action. + /// You can also choose to remind the user to complete the purchase if you detected a + /// [PurchaseWrapper] is still in the `pending` state in the future while calling [BillingClient.queryPurchases]. @JsonValue(2) pending, } 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 3be369885e4b..5e2ad151a416 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 @@ -69,8 +69,11 @@ class GooglePlayConnection @override Future completePurchase(PurchaseDetails purchase, - {String developerPayload}) { - return billingClient.acknowledgePurchase( + {String developerPayload}) async { + if (purchase.billingClientPurchase.isAcknowledged) { + return BillingResultWrapper(responseCode: BillingResponse.ok); + } + return await billingClient.acknowledgePurchase( purchase.verificationData.serverVerificationData, developerPayload: developerPayload); } From 1d41afc7bf293a9233b5c982a8a4c8f8bb378f61 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 2 Dec 2019 10:12:55 -0800 Subject: [PATCH 05/16] fix analyzer warning --- .../lib/src/in_app_purchase/google_play_connection.dart | 4 ---- 1 file changed, 4 deletions(-) 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 5e2ad151a416..d93826f8854a 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 @@ -297,8 +297,4 @@ class GooglePlayConnection return purchaseDetails; } - - static PurchaseStatus _buildPurchaseStatus(PurchaseStateWrapper state) { - return PurchaseStateConverter().toPurchaseStatus(state); - } } From 37df4a47b6dd56485dd42798595b7a02e55d7aa8 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 2 Dec 2019 10:35:52 -0800 Subject: [PATCH 06/16] review updates --- .../billing_client_wrapper.dart | 17 +++++++++-------- .../purchase_wrapper.dart | 1 + .../in_app_purchase_connection.dart | 8 ++++++-- 3 files changed, 16 insertions(+), 10 deletions(-) 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 2ca4dcd1dd51..04b353b06ffd 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 @@ -224,17 +224,18 @@ class BillingClient { /// Acknowledge an in-app purchase. /// - /// The developer is required to acknowledge that they have granted entitlement for all in-app purchases. + /// The developer must acknowledge all in-app purchases after they have been granted to the user. + /// If this doesn't happen within three days of the purchase, the purchase will be refunded. /// - /// Warning! The acknowledgement has to be happen within the 3 days of the purchase. - /// Failure to do so will result the purchase to be refunded. + /// Consumables are already implicitly acknowledged by calls to [consumeAsync] and + /// do not need to be explicitly acknowledged by using this method. + /// However this method can be called for them in order to explicitly acknowledge them if desired. /// - /// For consumable items, calling [consumeAsync] acts as an implicit acknowledgement. This method can also - /// be called for explicitly acknowledging a consumable purchase. + /// Be sure to only acknowledge a purchase after it has been granted to the user. + /// [PurchaseWrapper.purchaseState] should be [PurchaseStateWrapper.purchased] and + /// the purchase should be validated. See [Verify a purchase](https://developer.android.com/google/play/billing/billing_library_overview#Verify) on verifying purchases. /// - /// Be sure to only acknowledge a purchase when the [PurchaseWrapper.purchaseState] is [PurchaseStateWrapper.purchased]. - /// - /// Please refer to https://developer.android.com/google/play/billing/billing_library_overview#acknowledge for more + /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more /// details. /// /// The `purchaseToken` must not be null. 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 118d7502fe47..8680819b8cd2 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 @@ -279,6 +279,7 @@ enum PurchaseStateWrapper { /// The state is unspecified. /// /// No actions on the [PurchaseWrapper] should be performed on this state. + /// This is a catch-all. It should never be returned by the Play Billing Library. @JsonValue(0) unspecified_state, 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 7d45d165af9d..360158419c69 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 @@ -94,7 +94,7 @@ abstract class InAppPurchaseConnection { /// purchasing process. /// /// This method does return whether or not the purchase request was initially - /// sent succesfully. + /// sent successfully. /// /// Consumable items are defined differently by the different underlying /// payment platforms, and there's no way to query for whether or not the @@ -180,8 +180,12 @@ abstract class InAppPurchaseConnection { /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion. /// /// The method returns a [BillingResultWrapper] to indicate a detailed status of the complete process. + /// If the result contains [BillingResponse.error] or [BillingResponse.serviceUnavailable], the developer should try + /// to complete the purchase via this method again, or retry the [completePurchase] it at a later time. + /// If the result indicates other errors, there might be some issue with + /// the app's code. The developer is responsible to fix the issue. /// - /// Warning!Failure to call this method within 3 days of the purchase will result a refund on Android. + /// 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` only works on Android. From b0b69e5ff274ce6c156e3ab05502808939a38e40 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 4 Dec 2019 12:50:01 -0800 Subject: [PATCH 07/16] review fixes --- .../plugins/inapppurchase/TranslatorTest.java | 2 +- .../billing_client_wrappers/enum_converters.dart | 15 +++++++++------ .../in_app_purchase/google_play_connection.dart | 2 +- .../in_app_purchase_connection.dart | 2 +- .../lib/src/in_app_purchase/purchase_details.dart | 14 ++++++-------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 319b34ce787f..2ee1044fe0c5 100644 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -159,7 +159,7 @@ public void fromBillingResult() throws JSONException { } @Test - public void fromBillingResult_dubugMessageNull() throws JSONException { + public void fromBillingResult_debugMessageNull() throws JSONException { BillingResult newBillingResult = BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build(); Map billingResultMap = Translator.fromBillingResult(newBillingResult); diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart index 864f24c98e55..1e81895438c3 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart @@ -9,8 +9,9 @@ import 'package:json_annotation/json_annotation.dart'; part 'enum_converters.g.dart'; /// Serializer for [BillingResponse]. -// Use these in `@JsonSerializable()` classes by annotating them with -// `@BillingResponseConverter()`. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingResponseConverter()`. class BillingResponseConverter implements JsonConverter { const BillingResponseConverter(); @@ -23,8 +24,9 @@ class BillingResponseConverter implements JsonConverter { } /// Serializer for [SkuType]. -// Use these in `@JsonSerializable()` classes by annotating them with -// `@SkuTypeConverter()`. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SkuTypeConverter()`. class SkuTypeConverter implements JsonConverter { const SkuTypeConverter(); @@ -45,8 +47,9 @@ class _SerializedEnums { } /// Serializer for [PurchaseStateWrapper]. -// Use these in `@JsonSerializable()` classes by annotating them with -// `@PurchaseStateConverter()`. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@PurchaseStateConverter()`. class PurchaseStateConverter implements JsonConverter { const PurchaseStateConverter(); 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 d93826f8854a..865128f71282 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 @@ -259,7 +259,7 @@ class GooglePlayConnection return _maybeAutoConsumePurchase( PurchaseDetails.fromPurchase(purchase)..error = error); }).toList(); - if (!purchases.isEmpty) { + if (purchases.isNotEmpty) { return Future.wait(purchases); } else { return [ 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 360158419c69..feed83065437 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 @@ -185,7 +185,7 @@ abstract class InAppPurchaseConnection { /// If the result indicates other errors, there might be some issue with /// the app's code. The developer is responsible to fix the issue. /// - /// Warning!Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. + /// 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` only works on Android. diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index c8dd13df9768..8fbbc6af02f6 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -175,8 +175,8 @@ class PurchaseDetails { @required this.productID, @required this.verificationData, @required this.transactionDate, - this.skPaymentTransaction = null, - this.billingClientPurchase = null, + this.skPaymentTransaction, + this.billingClientPurchase, }); /// Generate a [PurchaseDetails] object based on an iOS [SKTransactionWrapper] object. @@ -194,9 +194,8 @@ class PurchaseDetails { this.skPaymentTransaction = transaction, this.billingClientPurchase = null, _status = SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState) { - _platform = _kPlatformIOS; - } + .toPurchaseStatus(transaction.transactionState), + _platform = _kPlatformIOS; /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. PurchaseDetails.fromPurchase(PurchaseWrapper purchase) @@ -210,9 +209,8 @@ class PurchaseDetails { this.skPaymentTransaction = null, this.billingClientPurchase = purchase, _status = - PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState) { - _platform = _kPlatformAndroid; - } + PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState), + _platform = _kPlatformAndroid; } /// The response object for fetching the past purchases. From fb8655e36a448d305716b20565b66f0ac345597c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 6 Dec 2019 12:00:13 -0800 Subject: [PATCH 08/16] fix test --- .../purchase_wrapper_test.dart | 14 ++++++++++++++ .../google_play_connection_test.dart | 10 +++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart index aad5ebd5eec2..6f65bdc9788d 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -22,6 +22,20 @@ final PurchaseWrapper dummyPurchase = PurchaseWrapper( purchaseState: PurchaseStateWrapper.purchased, ); +final PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( + orderId: 'orderId', + packageName: 'packageName', + purchaseTime: 0, + signature: 'signature', + sku: 'sku', + purchaseToken: 'purchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'dummy payload', + isAcknowledged: false, + purchaseState: PurchaseStateWrapper.purchased, +); + final PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = PurchaseHistoryRecordWrapper( purchaseTime: 0, 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 e6f4de06a2fd..f992c659c317 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 @@ -620,7 +620,7 @@ void main() { }); group('complete purchase', () { - const String consumeMethodName = + const String completeMethodName = 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('complete purchase success', () async { final BillingResponse expectedCode = BillingResponse.ok; @@ -628,11 +628,11 @@ void main() { final BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( - name: consumeMethodName, + name: completeMethodName, value: buildBillingResultMap(expectedBillingResult), ); PurchaseDetails purchaseDetails = - PurchaseDetails.fromPurchase(dummyPurchase); + PurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); Completer completer = Completer(); purchaseDetails.status = PurchaseStatus.purchased; if (purchaseDetails.pendingCompletePurchase) { @@ -640,6 +640,10 @@ void main() { await GooglePlayConnection.instance.completePurchase( purchaseDetails, developerPayload: 'dummy payload'); + print('pending ${billingResultWrapper.responseCode}'); + print('expectedBillingResult ${expectedBillingResult.responseCode}'); + print('pending ${billingResultWrapper.debugMessage}'); + print('expectedBillingResult ${expectedBillingResult.debugMessage}'); expect(billingResultWrapper, equals(expectedBillingResult)); completer.complete(billingResultWrapper); } From e815b17f8b187814ccd5dd08ba8962ea8db6714b Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 6 Dec 2019 15:03:19 -0800 Subject: [PATCH 09/16] add pending purchase api --- packages/in_app_purchase/CHANGELOG.md | 2 ++ .../inapppurchase/BillingClientFactory.java | 3 ++- .../BillingClientFactoryImpl.java | 11 ++++++----- .../inapppurchase/MethodCallHandlerImpl.java | 6 +++--- .../inapppurchase/MethodCallHandlerTest.java | 3 +-- .../billing_client_wrapper.dart | 18 +++++++++++++++++- .../google_play_connection.dart | 3 +++ .../in_app_purchase_connection.dart | 17 +++++++++++++++++ 8 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 26568ada0dba..0a3474f40614 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.3.0 * Migrate the `Google Play Library` to 2.0.3. + * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has + to call this method when initializing the `InAppPurchaseConnection` on Android. * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index b9ec9f6395a3..282fb97bbd43 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java @@ -17,7 +17,8 @@ interface BillingClientFactory { * * @param context The context used to create the {@link BillingClient}. * @param channel The method channel used to create the {@link BillingClient}. + * @param enablePendingPurchases Whether to enable pending purchases. Throws an exception if it is false. * @return The {@link BillingClient} object that is created. */ - BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel); + BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel, boolean enablePendingPurchases); } diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index af17bb071fc6..14f288f68d9f 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -12,10 +12,11 @@ final class BillingClientFactoryImpl implements BillingClientFactory { @Override - public BillingClient createBillingClient(Context context, MethodChannel channel) { - return BillingClient.newBuilder(context) - .enablePendingPurchases() - .setListener(new PluginPurchaseListener(channel)) - .build(); + public BillingClient createBillingClient(Context context, MethodChannel channel, boolean enablePendingPurchases) { + BillingClient.Builder builder = BillingClient.newBuilder(context); + if (enablePendingPurchases) { + builder.enablePendingPurchases(); + } + return builder.setListener(new PluginPurchaseListener(channel)).build(); } } 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 affc0cf8fe85..859443c2202e 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 @@ -73,7 +73,7 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { isReady(result); break; case InAppPurchasePlugin.MethodNames.START_CONNECTION: - startConnection((int) call.argument("handle"), result); + startConnection((int) call.argument("handle"),(boolean)call.argument("enablePendingPurchases"), result); break; case InAppPurchasePlugin.MethodNames.END_CONNECTION: endConnection(result); @@ -236,9 +236,9 @@ public void onPurchaseHistoryResponse( }); } - private void startConnection(final int handle, final MethodChannel.Result result) { + private void startConnection(final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) { if (billingClient == null) { - billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel); + billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel, enablePendingPurchases); } billingClient.startConnection( 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 14d0e560e0b0..6b8c5ffb81fa 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 @@ -76,8 +76,7 @@ public class MethodCallHandlerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - - factory = (context, channel) -> mockBillingClient; + factory = (context, channel, true) -> mockBillingClient; methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); } 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 04b353b06ffd..ca3ff9d3ffdf 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 @@ -49,6 +49,9 @@ typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult); /// some minor changes to account for language differences. Callbacks have been /// converted to futures where appropriate. class BillingClient { + + bool _enablePendingPurchases = false; + BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { assert(onPurchasesUpdated != null); channel.setMethodCallHandler(callHandler); @@ -71,6 +74,17 @@ class BillingClient { Future isReady() async => await channel.invokeMethod('BillingClient#isReady()'); + /// Enable the [BillingClientWrapper] to handle pending purchases. + /// + /// This method is required to be called when initialize the application. + /// It is to acknowledge your application has been updated to support pending purchases. + /// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending) + /// for more details. + /// Failure to call this method before any other method in the [startConnection] will throw an exception. + void enablePendingPurchases() { + _enablePendingPurchases = true; + } + /// Calls /// [`BillingClient#startConnection(BillingClientStateListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#startconnection) /// to create and connect a `BillingClient` instance. @@ -84,13 +98,15 @@ class BillingClient { Future startConnection( {@required OnBillingServiceDisconnected onBillingServiceDisconnected}) async { + assert(_enablePendingPurchases, 'enablePendingPurchases() must be called before calling startConnection'); List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); return BillingResultWrapper.fromJson(await channel .invokeMapMethod( "BillingClient#startConnection(BillingClientStateListener)", - {'handle': disconnectCallbacks.length - 1})); + {'handle': disconnectCallbacks.length - 1, + 'enablePendingPurchases': _enablePendingPurchases})); } /// Calls 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 329e5960868b..f2cd87b0700b 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 @@ -24,6 +24,9 @@ class GooglePlayConnection _purchaseUpdatedController .add(await _getPurchaseDetailsFromResult(resultWrapper)); }) { + if (InAppPurchaseConnection.enablePendingPurchase) { + billingClient.enablePendingPurchases(); + } _readyFuture = _connect(); WidgetsBinding.instance.addObserver(this); _purchaseUpdatedController = StreamController.broadcast(); 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 797092004753..9ee9e784303c 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 @@ -60,9 +60,26 @@ abstract class InAppPurchaseConnection { return _purchaseUpdatedStream; } + /// Whether pending purchase is enabled. + /// + /// See also [enablePendingPurchases] for more on pending purchases. + static bool get enablePendingPurchase => _enablePendingPurchase; + static bool _enablePendingPurchase = false; + /// Returns true if the payment platform is ready and available. Future isAvailable(); + /// Enable the [InAppPurchaseConnection] to handle pending purchases. + /// + /// Android Only: This method is required to be called when initialize the application. + /// It is to acknowledge your application has been updated to support pending purchases. + /// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending) + /// for more details. + /// Failure to call this method before access [instance] will throw an exception. + /// + /// It is an no-op on iOS. + static void enablePendingPurchases(){_enablePendingPurchase = true;} + /// Query product details for the given set of IDs. /// /// The [identifiers] need to exactly match existing configured product From 61c5771f68d41625d7d4fda7d892dab9bf35e763 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 6 Dec 2019 16:06:51 -0800 Subject: [PATCH 10/16] formatting --- .../plugins/inapppurchase/BillingClientFactory.java | 6 ++++-- .../inapppurchase/BillingClientFactoryImpl.java | 3 ++- .../plugins/inapppurchase/MethodCallHandlerImpl.java | 12 +++++++++--- packages/in_app_purchase/example/lib/main.dart | 1 + .../billing_client_wrapper.dart | 10 ++++++---- .../in_app_purchase/in_app_purchase_connection.dart | 4 +++- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index 282fb97bbd43..b320c17aa992 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java @@ -17,8 +17,10 @@ interface BillingClientFactory { * * @param context The context used to create the {@link BillingClient}. * @param channel The method channel used to create the {@link BillingClient}. - * @param enablePendingPurchases Whether to enable pending purchases. Throws an exception if it is false. + * @param enablePendingPurchases Whether to enable pending purchases. Throws an exception if it is + * false. * @return The {@link BillingClient} object that is created. */ - BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel, boolean enablePendingPurchases); + BillingClient createBillingClient( + @NonNull Context context, @NonNull MethodChannel channel, boolean enablePendingPurchases); } diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index 14f288f68d9f..9bfddaf57545 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -12,7 +12,8 @@ final class BillingClientFactoryImpl implements BillingClientFactory { @Override - public BillingClient createBillingClient(Context context, MethodChannel channel, boolean enablePendingPurchases) { + public BillingClient createBillingClient( + Context context, MethodChannel channel, boolean enablePendingPurchases) { BillingClient.Builder builder = BillingClient.newBuilder(context); if (enablePendingPurchases) { builder.enablePendingPurchases(); 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 859443c2202e..9108ab36bcd1 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 @@ -73,7 +73,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { isReady(result); break; case InAppPurchasePlugin.MethodNames.START_CONNECTION: - startConnection((int) call.argument("handle"),(boolean)call.argument("enablePendingPurchases"), result); + startConnection( + (int) call.argument("handle"), + (boolean) call.argument("enablePendingPurchases"), + result); break; case InAppPurchasePlugin.MethodNames.END_CONNECTION: endConnection(result); @@ -236,9 +239,12 @@ public void onPurchaseHistoryResponse( }); } - private void startConnection(final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) { + private void startConnection( + final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) { if (billingClient == null) { - billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel, enablePendingPurchases); + billingClient = + billingClientFactory.createBillingClient( + applicationContext, methodChannel, enablePendingPurchases); } billingClient.startConnection( diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 576dfd933b97..8e384e2acb5e 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -9,6 +9,7 @@ import 'package:in_app_purchase/in_app_purchase.dart'; import 'consumable_store.dart'; void main() { + InAppPurchaseConnection.enablePendingPurchases(); runApp(MyApp()); } 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 ca3ff9d3ffdf..ae2e25c8131c 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 @@ -49,7 +49,6 @@ typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult); /// some minor changes to account for language differences. Callbacks have been /// converted to futures where appropriate. class BillingClient { - bool _enablePendingPurchases = false; BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { @@ -98,15 +97,18 @@ class BillingClient { Future startConnection( {@required OnBillingServiceDisconnected onBillingServiceDisconnected}) async { - assert(_enablePendingPurchases, 'enablePendingPurchases() must be called before calling startConnection'); + assert(_enablePendingPurchases, + 'enablePendingPurchases() must be called before calling startConnection'); List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); return BillingResultWrapper.fromJson(await channel .invokeMapMethod( "BillingClient#startConnection(BillingClientStateListener)", - {'handle': disconnectCallbacks.length - 1, - 'enablePendingPurchases': _enablePendingPurchases})); + { + 'handle': disconnectCallbacks.length - 1, + 'enablePendingPurchases': _enablePendingPurchases + })); } /// Calls 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 9ee9e784303c..4c4953d1ce98 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 @@ -78,7 +78,9 @@ abstract class InAppPurchaseConnection { /// Failure to call this method before access [instance] will throw an exception. /// /// It is an no-op on iOS. - static void enablePendingPurchases(){_enablePendingPurchase = true;} + static void enablePendingPurchases() { + _enablePendingPurchase = true; + } /// Query product details for the given set of IDs. /// From d63cc132fb238eb459051bb6074448767f1fd90c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 11 Dec 2019 10:09:34 -0800 Subject: [PATCH 11/16] review comments fix --- packages/in_app_purchase/CHANGELOG.md | 2 +- packages/in_app_purchase/example/lib/main.dart | 3 +++ .../src/billing_client_wrappers/billing_client_wrapper.dart | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 0a3474f40614..d462699f9555 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -2,7 +2,7 @@ * Migrate the `Google Play Library` to 2.0.3. * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has - to call this method when initializing the `InAppPurchaseConnection` on Android. + to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 8e384e2acb5e..cf1a2a92a6d8 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -9,6 +9,9 @@ import 'package:in_app_purchase/in_app_purchase.dart'; import 'consumable_store.dart'; void main() { + // For play billing library 2.0 on Android, it is mandatory to call + // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) + // as part of initializing the app. InAppPurchaseConnection.enablePendingPurchases(); runApp(MyApp()); } 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 ae2e25c8131c..ebbd90aba0f4 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 @@ -75,10 +75,11 @@ class BillingClient { /// Enable the [BillingClientWrapper] to handle pending purchases. /// - /// This method is required to be called when initialize the application. + /// Play requires that you call this method when initializing your application. /// It is to acknowledge your application has been updated to support pending purchases. /// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending) /// for more details. + /// /// Failure to call this method before any other method in the [startConnection] will throw an exception. void enablePendingPurchases() { _enablePendingPurchases = true; From e610421a2aab20bb07ce1185de3bbb9e52dd1e96 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 11 Dec 2019 13:51:41 -0800 Subject: [PATCH 12/16] fix test for enablependingpurchases and formatting --- .../inapppurchase/MethodCallHandlerTest.java | 18 +++++++++++++----- packages/in_app_purchase/example/lib/main.dart | 3 +-- 2 files changed, 14 insertions(+), 7 deletions(-) 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 6b8c5ffb81fa..be00ac4e6e91 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 @@ -34,6 +34,7 @@ import android.app.Activity; import android.content.Context; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; @@ -76,7 +77,10 @@ public class MethodCallHandlerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - factory = (context, channel, true) -> mockBillingClient; + factory = + (@NonNull Context context, + @NonNull MethodChannel channel, + boolean enablePendingPurchases) -> mockBillingClient; methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); } @@ -133,8 +137,9 @@ public void startConnection() { @Test public void startConnection_multipleCalls() { - Map arguments = new HashMap<>(); + Map arguments = new HashMap<>(); arguments.put("handle", 1); + arguments.put("enablePendingPurchases", true); MethodCall call = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); @@ -170,8 +175,9 @@ public void startConnection_multipleCalls() { public void endConnection() { // Set up a connected BillingClient instance final int disconnectCallbackHandle = 22; - Map arguments = new HashMap<>(); + Map arguments = new HashMap<>(); arguments.put("handle", disconnectCallbackHandle); + arguments.put("enablePendingPurchases", true); MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); @@ -546,8 +552,9 @@ public void acknowledgePurchase() { } private ArgumentCaptor mockStartConnection() { - Map arguments = new HashMap<>(); + Map arguments = new HashMap<>(); arguments.put("handle", 1); + arguments.put("enablePendingPurchases", true); MethodCall call = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); @@ -558,10 +565,11 @@ private ArgumentCaptor mockStartConnection() { } private void establishConnectedBillingClient( - @Nullable Map arguments, @Nullable Result result) { + @Nullable Map arguments, @Nullable Result result) { if (arguments == null) { arguments = new HashMap<>(); arguments.put("handle", 1); + arguments.put("enablePendingPurchases", true); } if (result == null) { result = mock(Result.class); diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 6eb28a3f260a..826d2a121cdc 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -365,8 +365,7 @@ class _MyAppState extends State { void _listenToPurchaseUpdated(List purchaseDetailsList) { purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { - await InAppPurchaseConnection.instance - .consumePurchase(purchaseDetails); + await InAppPurchaseConnection.instance.consumePurchase(purchaseDetails); if (purchaseDetails.status == PurchaseStatus.pending) { showPendingUI(); } else { From a3239f7e2a60c5c4e7b2d3b3ee5280f3e9f6ee7f Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 13 Dec 2019 15:37:38 -0800 Subject: [PATCH 13/16] add missing breaking change in CHANGELOG --- packages/in_app_purchase/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 3b8bd62827b0..3e3ac1e68b35 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,8 +1,6 @@ ## 0.3.0 * Migrate the `Google Play Library` to 2.0.3. - * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has - to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. @@ -12,10 +10,12 @@ * Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. * **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed", it is implicitly acknowledged. + * **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. * Updates to the "InAppPurchaseConnection": * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. * **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. * A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. + * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. * Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes. * Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update. From 33576c74de3366cb680f336c6bc31e722fa1cccf Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 23 Dec 2019 15:00:49 -0800 Subject: [PATCH 14/16] fix tests --- .../billing_client_wrappers/billing_client_wrapper_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d847757ae26f..d0c9b00f4bbd 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 @@ -23,6 +23,7 @@ void main() { setUp(() { billingClient = BillingClient((PurchasesResultWrapper _) {}); + billingClient.enablePendingPurchases(); stubPlatform.reset(); }); @@ -72,7 +73,7 @@ void main() { ); await billingClient.startConnection(onBillingServiceDisconnected: () {}); final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect(call.arguments, equals({'handle': 0})); + expect(call.arguments, equals({'handle': 0, 'enablePendingPurchases': true})); }); }); From a3a576ca9142f9cce2cd8c6dbc91e51d00de75e4 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 23 Dec 2019 15:01:13 -0800 Subject: [PATCH 15/16] formatting --- .../billing_client_wrappers/billing_client_wrapper_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 d0c9b00f4bbd..54f7c3eda77f 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 @@ -73,7 +73,10 @@ void main() { ); await billingClient.startConnection(onBillingServiceDisconnected: () {}); final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect(call.arguments, equals({'handle': 0, 'enablePendingPurchases': true})); + expect( + call.arguments, + equals( + {'handle': 0, 'enablePendingPurchases': true})); }); }); From 6f2c8213afecbbc8df97d4a2f3d92e4a031782ec Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 23 Dec 2019 15:12:29 -0800 Subject: [PATCH 16/16] update CHANGELOG --- packages/in_app_purchase/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 38a4f78b3b7c..1287dea28122 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -9,10 +9,10 @@ * Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields. * Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. * **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. - * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed", it is implicitly acknowledged. + * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed" via `BillingClient.consumeAsync`, it is implicitly acknowledged. * **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. * Updates to the "InAppPurchaseConnection": - * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. + * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. If a purchase is not completed within 3 days on Android, the user will be refunded. * **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. * A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information.