-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[in_app_purchase] migrate playing billing library to v3 #3636
Changes from 10 commits
d73dcc5
ea34ac4
d6e49d3
34ecb8b
65a3cbd
102d7cd
d6b65db
df3a45b
822e631
ac4348c
99b4eef
763360d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,12 @@ | ||
| ## 0.5.0 | ||
|
|
||
| * Migrate to Google Billing Library 3.0 | ||
| * Add `obfuscatedProfileId`, `purchaseToken` in [BillingClientWrapper.launchBillingFlow]. | ||
| * Removed `developerPayload` in [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase]. | ||
| * **Breaking Change** | ||
| * Removed `isRewarded` from [SkuDetailsWrapper]. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any useful context to provide about why they are removed? E.g., link to discussion of removal in the SDK?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
| * [SkuDetailsWrapper.introductoryPriceCycles] now returns `int` instead of `String`. | ||
|
|
||
| ## 0.4.1 | ||
|
|
||
| * Support InApp subscription upgrade/downgrade. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,9 +35,9 @@ android { | |
|
|
||
| dependencies { | ||
| implementation 'androidx.annotation:annotation:1.0.0' | ||
| implementation 'com.android.billingclient:billing:2.0.3' | ||
| implementation 'com.android.billingclient:billing:3.0.2' | ||
| testImplementation 'junit:junit:4.12' | ||
| testImplementation 'org.mockito:mockito-core:2.17.0' | ||
| testImplementation 'org.mockito:mockito-core:3.6.0' | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is required to mock the objects in the new PBL. |
||
| androidTestImplementation 'androidx.test:runner:1.1.1' | ||
| androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| import static java.util.stream.Collectors.toList; | ||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertNull; | ||
| import static org.junit.Assert.fail; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.contains; | ||
| import static org.mockito.ArgumentMatchers.eq; | ||
|
|
@@ -60,6 +61,7 @@ | |
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.json.JSONException; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
| import org.mockito.ArgumentCaptor; | ||
|
|
@@ -79,7 +81,7 @@ public class MethodCallHandlerTest { | |
|
|
||
| @Before | ||
| public void setUp() { | ||
| MockitoAnnotations.initMocks(this); | ||
| MockitoAnnotations.openMocks(this); | ||
| factory = | ||
| (@NonNull Context context, | ||
| @NonNull MethodChannel channel, | ||
|
|
@@ -269,6 +271,7 @@ public void launchBillingFlow_ok_null_AccountId() { | |
| HashMap<String, Object> arguments = new HashMap<>(); | ||
| arguments.put("sku", skuId); | ||
| arguments.put("accountId", null); | ||
| arguments.put("obfuscatedProfileId", null); | ||
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); | ||
|
|
||
| // Launch the billing flow | ||
|
|
@@ -286,7 +289,6 @@ public void launchBillingFlow_ok_null_AccountId() { | |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); | ||
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); | ||
| assertEquals(params.getSku(), skuId); | ||
| assertNull(params.getAccountId()); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getAccountId is not available anymore so we can't really test this value, however, we can still test if the call crashes, so ill leave the test here
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you rename the test then, to better explain what it's doing now? (Probably needs a comment in addition to a new name.)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
|
||
| // Verify we pass the response code to result | ||
| verify(result, never()).error(any(), any(), any()); | ||
|
|
@@ -320,7 +322,6 @@ public void launchBillingFlow_ok_null_OldSku() { | |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); | ||
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); | ||
| assertEquals(params.getSku(), skuId); | ||
| assertEquals(params.getAccountId(), accountId); | ||
| assertNull(params.getOldSku()); | ||
| // Verify we pass the response code to result | ||
| verify(result, never()).error(any(), any(), any()); | ||
|
|
@@ -374,7 +375,6 @@ public void launchBillingFlow_ok_oldSku() { | |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); | ||
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); | ||
| assertEquals(params.getSku(), skuId); | ||
| assertEquals(params.getAccountId(), accountId); | ||
| assertEquals(params.getOldSku(), oldSkuId); | ||
|
|
||
| // Verify we pass the response code to result | ||
|
|
@@ -408,7 +408,6 @@ public void launchBillingFlow_ok_AccountId() { | |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); | ||
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); | ||
| assertEquals(params.getSku(), skuId); | ||
| assertEquals(params.getAccountId(), accountId); | ||
|
|
||
| // Verify we pass the response code to result | ||
| verify(result, never()).error(any(), any(), any()); | ||
|
|
@@ -420,13 +419,15 @@ public void launchBillingFlow_ok_Proration() { | |
| // Fetch the sku details first and query the method call | ||
| String skuId = "foo"; | ||
| String oldSkuId = "oldFoo"; | ||
| String purchaseToken = "purchaseTokenFoo"; | ||
| String accountId = "account"; | ||
| int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; | ||
| queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); | ||
| HashMap<String, Object> arguments = new HashMap<>(); | ||
| arguments.put("sku", skuId); | ||
| arguments.put("accountId", accountId); | ||
| arguments.put("oldSku", oldSkuId); | ||
| arguments.put("purchaseToken", purchaseToken); | ||
| arguments.put("prorationMode", prorationMode); | ||
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); | ||
|
|
||
|
|
@@ -445,8 +446,8 @@ public void launchBillingFlow_ok_Proration() { | |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); | ||
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); | ||
| assertEquals(params.getSku(), skuId); | ||
| assertEquals(params.getAccountId(), accountId); | ||
| assertEquals(params.getOldSku(), oldSkuId); | ||
| assertEquals(params.getOldSkuPurchaseToken(), purchaseToken); | ||
| assertEquals(params.getReplaceSkusProrationMode(), prorationMode); | ||
|
|
||
| // Verify we pass the response code to result | ||
|
|
@@ -668,11 +669,7 @@ public void consumeAsync() { | |
|
|
||
| methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); | ||
|
|
||
| ConsumeParams params = | ||
| ConsumeParams.newBuilder() | ||
| .setDeveloperPayload("mockPayload") | ||
| .setPurchaseToken("mockToken") | ||
| .build(); | ||
| ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken("mockToken").build(); | ||
|
|
||
| // Verify we pass the data to result | ||
| verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); | ||
|
|
@@ -703,10 +700,7 @@ public void acknowledgePurchase() { | |
| methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); | ||
|
|
||
| AcknowledgePurchaseParams params = | ||
| AcknowledgePurchaseParams.newBuilder() | ||
| .setDeveloperPayload("mockPayload") | ||
| .setPurchaseToken("mockToken") | ||
| .build(); | ||
| AcknowledgePurchaseParams.newBuilder().setPurchaseToken("mockToken").build(); | ||
|
|
||
| // Verify we pass the data to result | ||
| verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); | ||
|
|
@@ -774,6 +768,7 @@ private void queryForSkus(List<String> skusList) { | |
| verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); | ||
| List<SkuDetails> skuDetailsResponse = | ||
| skusList.stream().map(this::buildSkuDetails).collect(toList()); | ||
|
|
||
| BillingResult billingResult = | ||
| BillingResult.newBuilder() | ||
| .setResponseCode(100) | ||
|
|
@@ -783,8 +778,16 @@ private void queryForSkus(List<String> skusList) { | |
| } | ||
|
|
||
| private SkuDetails buildSkuDetails(String id) { | ||
| SkuDetails details = mock(SkuDetails.class); | ||
| when(details.getSku()).thenReturn(id); | ||
| String json = | ||
| String.format( | ||
| "{\"packageName\": \"dummyPackageName\",\"productId\":\"%s\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}", | ||
| id); | ||
| SkuDetails details = null; | ||
| try { | ||
| details = new SkuDetails(json); | ||
| } catch (JSONException e) { | ||
| fail("buildSkuDetails failed with JSONException " + e.toString()); | ||
| } | ||
| return details; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -155,8 +155,13 @@ class BillingClient { | |
| /// The [skuDetails] needs to have already been fetched in a [querySkuDetails] | ||
| /// call. The [accountId] is an optional hashed string associated with the user | ||
| /// that's unique to your app. It's used by Google to detect unusual behavior. | ||
| /// Do not pass in a cleartext [accountId], use your developer ID, or use the | ||
| /// user's Google ID for this field. | ||
| /// Do not pass in a cleartext [accountId], Do not use this field to store any Personally Identifiable Information (PII) | ||
|
||
| /// such as emails in cleartext. Attempting to store PII in this field will result in purchases being blocked. | ||
| /// Google Play recommends that you use either encryption or a one-way hash to generate an obfuscated identifier to send to Google Play. | ||
| /// | ||
| /// Specifies an optional [obfuscatedProfileId] that is uniquely associated with the user's profile in your app. | ||
|
||
| /// Some applications allow users to have multiple profiles within a single account. Use this method to send the user's profile identifier to Google. | ||
| /// Setting this field requests the user's obfuscated account id. | ||
| /// | ||
| /// Calling this attemps to show the Google Play purchase UI. The user is free | ||
| /// to complete the transaction there. | ||
|
|
@@ -171,25 +176,31 @@ class BillingClient { | |
| /// [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) | ||
| /// instance by [setting the given | ||
| /// skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails) | ||
| /// and [the given | ||
| /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)). | ||
| /// , [the given | ||
|
||
| /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)) | ||
| /// and the [obfuscatedProfileId] (https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid). | ||
| /// | ||
| /// When this method is called to purchase a subscription, an optional `oldSku` | ||
| /// can be passed in. This will tell Google Play that rather than purchasing a new subscription, | ||
| /// the user needs to upgrade/downgrade the existing subscription. | ||
| /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) is the SKU id that the user is upgrading or downgrading from. | ||
| /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) and [purchaseToken] are the SKU id and purchase token that the user is upgrading or downgrading from. | ||
| /// [purchaseToken] must not be `null` if [oldSku] is not `null`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be asserted.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setreplaceskusprorationmode) is the mode of proration during subscription upgrade/downgrade. | ||
| /// This value will only be effective if the `oldSku` is also set. | ||
| Future<BillingResultWrapper> launchBillingFlow( | ||
| {required String sku, | ||
| String? accountId, | ||
| String? obfuscatedProfileId, | ||
| String? oldSku, | ||
| String? purchaseToken, | ||
| ProrationMode? prorationMode}) async { | ||
| assert(sku != null); | ||
| final Map<String, dynamic> arguments = <String, dynamic>{ | ||
| 'sku': sku, | ||
| 'accountId': accountId, | ||
| 'obfuscatedProfileId': obfuscatedProfileId, | ||
| 'oldSku': oldSku, | ||
| 'purchaseToken': purchaseToken, | ||
| 'prorationMode': ProrationModeConverter().toJson(prorationMode ?? | ||
| ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) | ||
| }; | ||
|
|
@@ -250,18 +261,14 @@ class BillingClient { | |
| /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. | ||
| /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. | ||
| /// | ||
| /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. | ||
| /// | ||
| /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) | ||
| Future<BillingResultWrapper> consumeAsync(String purchaseToken, | ||
| {String? developerPayload}) async { | ||
| Future<BillingResultWrapper> consumeAsync(String purchaseToken) async { | ||
| assert(purchaseToken != null); | ||
| return BillingResultWrapper.fromJson((await channel | ||
| .invokeMapMethod<String, dynamic>( | ||
| 'BillingClient#consumeAsync(String, ConsumeResponseListener)', | ||
| <String, dynamic>{ | ||
| 'purchaseToken': purchaseToken, | ||
| 'developerPayload': developerPayload, | ||
| })) ?? | ||
| <String, dynamic>{}); | ||
| } | ||
|
|
@@ -282,18 +289,14 @@ class BillingClient { | |
| /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more | ||
| /// details. | ||
| /// | ||
| /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. | ||
| /// | ||
| /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) | ||
| Future<BillingResultWrapper> acknowledgePurchase(String purchaseToken, | ||
| {String? developerPayload}) async { | ||
| Future<BillingResultWrapper> acknowledgePurchase(String purchaseToken) async { | ||
| assert(purchaseToken != null); | ||
| return BillingResultWrapper.fromJson((await channel.invokeMapMethod<String, | ||
| dynamic>( | ||
| 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', | ||
| <String, dynamic>{ | ||
| 'purchaseToken': purchaseToken, | ||
| 'developerPayload': developerPayload, | ||
| })) ?? | ||
| <String, dynamic>{}); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,6 +120,8 @@ class PurchaseWrapper { | |
| /// The payload specified by the developer when the purchase was acknowledged or consumed. | ||
| /// | ||
| /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. | ||
| /// The `developerPayload` is removed from [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase] | ||
| /// after plugin version `0.5.0`. As a result, this will be `null` for new purchases happened after `0.5.0`. | ||
|
||
| final String? developerPayload; | ||
|
|
||
| /// Whether the purchase has been acknowledged. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These aren't breaking changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh they are. Fixed