Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/in_app_purchase/CHANGELOG.md
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].
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh they are. Fixed

* **Breaking Change**
* Removed `isRewarded` from [SkuDetailsWrapper].
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand Down
4 changes: 2 additions & 2 deletions packages/in_app_purchase/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -125,7 +125,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
launchBillingFlow(
(String) call.argument("sku"),
(String) call.argument("accountId"),
(String) call.argument("obfuscatedProfileId"),
(String) call.argument("oldSku"),
(String) call.argument("purchaseToken"),
call.hasArgument("prorationMode")
? (int) call.argument("prorationMode")
: ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY,
Expand All @@ -138,16 +140,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
queryPurchaseHistoryAsync((String) call.argument("skuType"), result);
break;
case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC:
consumeAsync(
(String) call.argument("purchaseToken"),
(String) call.argument("developerPayload"),
result);
consumeAsync((String) call.argument("purchaseToken"), result);
break;
case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE:
acknowledgePurchase(
(String) call.argument("purchaseToken"),
(String) call.argument("developerPayload"),
result);
acknowledgePurchase((String) call.argument("purchaseToken"), result);
break;
default:
result.notImplemented();
Expand Down Expand Up @@ -200,7 +196,9 @@ public void onSkuDetailsResponse(
private void launchBillingFlow(
String sku,
@Nullable String accountId,
@Nullable String obfuscatedProfileId,
@Nullable String oldSku,
@Nullable String purchaseToken,
int prorationMode,
MethodChannel.Result result) {
if (billingClientError(result)) {
Expand Down Expand Up @@ -248,10 +246,13 @@ private void launchBillingFlow(
BillingFlowParams.Builder paramsBuilder =
BillingFlowParams.newBuilder().setSkuDetails(skuDetails);
if (accountId != null && !accountId.isEmpty()) {
paramsBuilder.setAccountId(accountId);
paramsBuilder.setObfuscatedAccountId(accountId);
}
if (obfuscatedProfileId != null && !obfuscatedProfileId.isEmpty()) {
paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId);
}
if (oldSku != null && !oldSku.isEmpty()) {
paramsBuilder.setOldSku(oldSku);
paramsBuilder.setOldSku(oldSku, purchaseToken);
}
// The proration mode value has to match one of the following declared in
// https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode
Expand All @@ -261,8 +262,7 @@ private void launchBillingFlow(
billingClient.launchBillingFlow(activity, paramsBuilder.build())));
}

private void consumeAsync(
String purchaseToken, String developerPayload, final MethodChannel.Result result) {
private void consumeAsync(String purchaseToken, final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
Expand All @@ -277,9 +277,6 @@ public void onConsumeResponse(BillingResult billingResult, String outToken) {
ConsumeParams.Builder paramsBuilder =
ConsumeParams.newBuilder().setPurchaseToken(purchaseToken);

if (developerPayload != null) {
paramsBuilder.setDeveloperPayload(developerPayload);
}
ConsumeParams params = paramsBuilder.build();

billingClient.consumeAsync(params, listener);
Expand Down Expand Up @@ -348,16 +345,12 @@ public void onBillingServiceDisconnected() {
});
}

private void acknowledgePurchase(
String purchaseToken, @Nullable String developerPayload, final MethodChannel.Result result) {
private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
AcknowledgePurchaseParams params =
AcknowledgePurchaseParams.newBuilder()
.setDeveloperPayload(developerPayload)
.setPurchaseToken(purchaseToken)
.build();
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build();
billingClient.acknowledgePurchase(
params,
new AcknowledgePurchaseResponseListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ static HashMap<String, Object> fromSkuDetail(SkuDetails detail) {
info.put("priceCurrencyCode", detail.getPriceCurrencyCode());
info.put("sku", detail.getSku());
info.put("type", detail.getType());
info.put("isRewarded", detail.isRewarded());
info.put("subscriptionPeriod", detail.getSubscriptionPeriod());
info.put("originalPrice", detail.getOriginalPrice());
info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros());
Expand Down
4 changes: 2 additions & 2 deletions packages/in_app_purchase/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ flutter {
}

dependencies {
implementation 'com.android.billingclient:billing:1.2'
implementation 'com.android.billingclient:billing:3.0.2'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.17.0'
testImplementation 'org.mockito:mockito-core:3.6.0'
testImplementation 'org.json:json:20180813'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -79,7 +81,7 @@ public class MethodCallHandlerTest {

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
MockitoAnnotations.openMocks(this);
factory =
(@NonNull Context context,
@NonNull MethodChannel channel,
Expand Down Expand Up @@ -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
Expand All @@ -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());
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand All @@ -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);

Expand All @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: s/Do not use/and do not use/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Fix double space after ]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// 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.
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is hard to read; let's put the comma on the previous line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// 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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be asserted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
};
Expand Down Expand Up @@ -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>{});
}
Expand All @@ -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>{});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/happened after/that happen after updating to/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

final String? developerPayload;

/// Whether the purchase has been acknowledged.
Expand Down
Loading