Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[in_app_purchase] Add support to cross-grade a subscription on Android #2724

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ Giancarlo Rocha <giancarloiff@gmail.com>
Ryo Miyake <ryo@miyake.id>
Théo Champion <contact.theochampion@gmail.com>
Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
Eitan Schwartz <eshvartz@gmail.com>
Eitan Schwartz <eshvartz@gmail.com>
Alessandro Agosto <agosto.alessandro@gmail.com>
6 changes: 5 additions & 1 deletion packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.4

* Android: Add support for subscription cross-grades

## 0.3.3

* Introduce `SKPaymentQueueWrapper.transactions`.
Expand All @@ -9,7 +13,7 @@
## 0.3.2+1

* iOS: Fix only transactions with SKPaymentTransactionStatePurchased and SKPaymentTransactionStateFailed can be finished.
* iOS: Only one pending transaction of a given product is allowed.
* iOS: Only one pending transaction of a given product is allowed.

## 0.3.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
break;
case InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW:
launchBillingFlow(
(String) call.argument("sku"), (String) call.argument("accountId"), result);
(String) call.argument("sku"),
(String) call.argument("accountId"),
(String) call.argument("oldSku"),
result);
break;
case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES:
queryPurchases((String) call.argument("skuType"), result);
Expand Down Expand Up @@ -189,7 +192,10 @@ public void onSkuDetailsResponse(
}

private void launchBillingFlow(
String sku, @Nullable String accountId, MethodChannel.Result result) {
String sku,
@Nullable String accountId,
@Nullable String oldSku,
MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
Expand All @@ -203,6 +209,19 @@ private void launchBillingFlow(
return;
}

SkuDetails oldSkuDetails = null;

if (oldSku != null) {
oldSkuDetails = cachedSkus.get(oldSku);
if (oldSkuDetails == null) {
result.error(
"NOT_FOUND",
"Details for sku " + oldSku + " are not available. Has this ID already been fetched?",
null);
return;
}
}

if (activity == null) {
result.error(
"ACTIVITY_UNAVAILABLE",
Expand All @@ -213,8 +232,17 @@ private void launchBillingFlow(
return;
}

BillingFlowParams.Builder paramsBuilder =
BillingFlowParams.newBuilder().setSkuDetails(skuDetails);
BillingFlowParams.Builder paramsBuilder;

// Requested a subscription cross-grade.
if (oldSkuDetails != null) {
// NOTE: currently only the default proration mode is supported.
// https://developer.android.com/google/play/billing/billing_subscriptions#set-proration-mode
paramsBuilder = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).setOldSku(oldSku);
} else {
paramsBuilder = BillingFlowParams.newBuilder().setSkuDetails(skuDetails);
}

if (accountId != null && !accountId.isEmpty()) {
paramsBuilder.setAccountId(accountId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ class BillingClient {
/// and [the given
/// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)).
Future<BillingResultWrapper> launchBillingFlow(
{@required String sku, String accountId}) async {
{@required String sku, String accountId, String oldSku}) async {
assert(sku != null);
final Map<String, dynamic> arguments = <String, dynamic>{
'sku': sku,
'accountId': accountId,
'oldSku': oldSku,
};
return BillingResultWrapper.fromJson(
await channel.invokeMapMethod<String, dynamic>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class GooglePlayConnection
BillingResultWrapper billingResultWrapper =
await billingClient.launchBillingFlow(
sku: purchaseParam.productDetails.id,
accountId: purchaseParam.applicationUserName);
accountId: purchaseParam.applicationUserName,
oldSku: purchaseParam.oldProduct?.id);
return billingResultWrapper.responseCode == BillingResponse.ok;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class PurchaseParam {
PurchaseParam(
{@required this.productDetails,
this.applicationUserName,
this.sandboxTesting});
this.sandboxTesting,
this.oldProduct});

/// The product to create payment for.
///
Expand All @@ -96,6 +97,10 @@ class PurchaseParam {

/// The 'sandboxTesting' is only available on iOS, set it to `true` for testing in AppStore's sandbox environment. The default value is `false`.
final bool sandboxTesting;

/// The 'oldProduct' is only available on Android for non-consumable resources and is meant to allow subscription cross-grades.
/// By setting this you will replace the subscription represented by `oldProduct`. On iOS this is ignored.
final ProductDetails oldProduct;
}

/// Represents the transaction details of a purchase.
Expand Down
2 changes: 1 addition & 1 deletion packages/in_app_purchase/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,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.
homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase
version: 0.3.3
version: 0.3.4

dependencies:
async: ^2.0.8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ void main() {
stubPlatform.previousCallMatching(launchMethodName).arguments;
expect(arguments['sku'], equals(skuDetails.sku));
expect(arguments['accountId'], equals(accountId));
expect(arguments['oldSku'], isNull);
});

test('handles null accountId', () async {
Expand All @@ -178,6 +179,32 @@ void main() {
stubPlatform.previousCallMatching(launchMethodName).arguments;
expect(arguments['sku'], equals(skuDetails.sku));
expect(arguments['accountId'], isNull);
expect(arguments['oldSku'], isNull);
});

test('handles oldSku', () async {
const String debugMessage = 'dummy message';
final BillingResponse responseCode = BillingResponse.ok;
final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
);
final SkuDetailsWrapper skuDetails = dummySkuDetails;
final String accountId = "hashedAccountId";

expect(
await billingClient.launchBillingFlow(
sku: skuDetails.sku,
accountId: accountId,
oldSku: skuDetails.sku),
equals(expectedBillingResult));
Map<dynamic, dynamic> arguments =
stubPlatform.previousCallMatching(launchMethodName).arguments;
expect(arguments['sku'], equals(skuDetails.sku));
expect(arguments['accountId'], equals(accountId));
expect(arguments['oldSku'], equals(skuDetails.sku));
});
});

Expand Down