Skip to content

Commit 730129a

Browse files
authored
[in_app_purchase] billingClient launchPriceChangeConfirmationFlow (flutter#4077)
1 parent 9a90a9d commit 730129a

File tree

10 files changed

+274
-6
lines changed

10 files changed

+274
-6
lines changed

packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.1.4
2+
3+
* Added support for launchPriceChangeConfirmationFlow in the BillingClientWrapper and in InAppPurchaseAndroidPlatformAddition.
4+
15
## 0.1.3+1
26

37
* Add payment proxy.

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ static final class MethodNames {
4646
static final String ACKNOWLEDGE_PURCHASE =
4747
"BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
4848
static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
49+
static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW =
50+
"BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)";
4951

5052
private MethodNames() {};
5153
}

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.android.billingclient.api.BillingResult;
2525
import com.android.billingclient.api.ConsumeParams;
2626
import com.android.billingclient.api.ConsumeResponseListener;
27+
import com.android.billingclient.api.PriceChangeFlowParams;
2728
import com.android.billingclient.api.PurchaseHistoryRecord;
2829
import com.android.billingclient.api.PurchaseHistoryResponseListener;
2930
import com.android.billingclient.api.SkuDetails;
@@ -41,7 +42,7 @@ class MethodCallHandlerImpl
4142

4243
private static final String TAG = "InAppPurchasePlugin";
4344
private static final String LOAD_SKU_DOC_URL =
44-
"https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/README.md#loading-products-for-sale";
45+
"https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale";
4546

4647
@Nullable private BillingClient billingClient;
4748
private final BillingClientFactory billingClientFactory;
@@ -148,6 +149,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
148149
case InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED:
149150
isFeatureSupported((String) call.argument("feature"), result);
150151
break;
152+
case InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW:
153+
launchPriceChangeConfirmationFlow((String) call.argument("sku"), result);
154+
break;
151155
default:
152156
result.notImplemented();
153157
}
@@ -374,6 +378,44 @@ private void updateCachedSkus(@Nullable List<SkuDetails> skuDetailsList) {
374378
}
375379
}
376380

381+
private void launchPriceChangeConfirmationFlow(String sku, MethodChannel.Result result) {
382+
if (activity == null) {
383+
result.error(
384+
"ACTIVITY_UNAVAILABLE",
385+
"launchPriceChangeConfirmationFlow is not available. "
386+
+ "This method must be run with the app in foreground.",
387+
null);
388+
return;
389+
}
390+
if (billingClientError(result)) {
391+
return;
392+
}
393+
// Note that assert doesn't work on Android (see https://stackoverflow.com/a/6176529/5167831 and https://stackoverflow.com/a/8164195/5167831)
394+
// and that this assert is only added to silence the analyser. The actual null check
395+
// is handled by the `billingClientError()` call.
396+
assert billingClient != null;
397+
398+
SkuDetails skuDetails = cachedSkus.get(sku);
399+
if (skuDetails == null) {
400+
result.error(
401+
"NOT_FOUND",
402+
String.format(
403+
"Details for sku %s are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: %s",
404+
sku, LOAD_SKU_DOC_URL),
405+
null);
406+
return;
407+
}
408+
409+
PriceChangeFlowParams params =
410+
new PriceChangeFlowParams.Builder().setSkuDetails(skuDetails).build();
411+
billingClient.launchPriceChangeConfirmationFlow(
412+
activity,
413+
params,
414+
billingResult -> {
415+
result.success(Translator.fromBillingResult(billingResult));
416+
});
417+
}
418+
377419
private boolean billingClientError(MethodChannel.Result result) {
378420
if (billingClient != null) {
379421
return false;

packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED;
1111
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY;
1212
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW;
13+
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW;
1314
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT;
1415
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED;
1516
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES;
@@ -52,6 +53,8 @@
5253
import com.android.billingclient.api.BillingResult;
5354
import com.android.billingclient.api.ConsumeParams;
5455
import com.android.billingclient.api.ConsumeResponseListener;
56+
import com.android.billingclient.api.PriceChangeConfirmationListener;
57+
import com.android.billingclient.api.PriceChangeFlowParams;
5558
import com.android.billingclient.api.Purchase;
5659
import com.android.billingclient.api.Purchase.PurchasesResult;
5760
import com.android.billingclient.api.PurchaseHistoryRecord;
@@ -722,7 +725,7 @@ public void acknowledgePurchase() {
722725
}
723726

724727
@Test
725-
public void endConnection_if_activity_dettached() {
728+
public void endConnection_if_activity_detached() {
726729
InAppPurchasePlugin plugin = new InAppPurchasePlugin();
727730
plugin.setMethodCallHandler(methodChannelHandler);
728731
mockStartConnection();
@@ -768,6 +771,97 @@ public void isFutureSupported_false() {
768771
verify(result).success(false);
769772
}
770773

774+
@Test
775+
public void launchPriceChangeConfirmationFlow() {
776+
// Set up the sku details
777+
establishConnectedBillingClient(null, null);
778+
String skuId = "foo";
779+
queryForSkus(singletonList(skuId));
780+
781+
BillingResult billingResult =
782+
BillingResult.newBuilder()
783+
.setResponseCode(BillingClient.BillingResponseCode.OK)
784+
.setDebugMessage("dummy debug message")
785+
.build();
786+
787+
// Set up the mock billing client
788+
ArgumentCaptor<PriceChangeConfirmationListener> priceChangeConfirmationListenerArgumentCaptor =
789+
ArgumentCaptor.forClass(PriceChangeConfirmationListener.class);
790+
ArgumentCaptor<PriceChangeFlowParams> priceChangeFlowParamsArgumentCaptor =
791+
ArgumentCaptor.forClass(PriceChangeFlowParams.class);
792+
doNothing()
793+
.when(mockBillingClient)
794+
.launchPriceChangeConfirmationFlow(
795+
any(),
796+
priceChangeFlowParamsArgumentCaptor.capture(),
797+
priceChangeConfirmationListenerArgumentCaptor.capture());
798+
799+
// Call the methodChannelHandler
800+
HashMap<String, Object> arguments = new HashMap<>();
801+
arguments.put("sku", skuId);
802+
methodChannelHandler.onMethodCall(
803+
new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
804+
805+
// Verify the price change params.
806+
PriceChangeFlowParams priceChangeFlowParams = priceChangeFlowParamsArgumentCaptor.getValue();
807+
assertEquals(skuId, priceChangeFlowParams.getSkuDetails().getSku());
808+
809+
// Set the response in the callback
810+
PriceChangeConfirmationListener priceChangeConfirmationListener =
811+
priceChangeConfirmationListenerArgumentCaptor.getValue();
812+
priceChangeConfirmationListener.onPriceChangeConfirmationResult(billingResult);
813+
814+
// Verify we pass the response to result
815+
verify(result, never()).error(any(), any(), any());
816+
ArgumentCaptor<HashMap> resultCaptor = ArgumentCaptor.forClass(HashMap.class);
817+
verify(result, times(1)).success(resultCaptor.capture());
818+
assertEquals(fromBillingResult(billingResult), resultCaptor.getValue());
819+
}
820+
821+
@Test
822+
public void launchPriceChangeConfirmationFlow_withoutActivity_returnsActivityUnavailableError() {
823+
// Set up the sku details
824+
establishConnectedBillingClient(null, null);
825+
String skuId = "foo";
826+
queryForSkus(singletonList(skuId));
827+
828+
methodChannelHandler.setActivity(null);
829+
830+
// Call the methodChannelHandler
831+
HashMap<String, Object> arguments = new HashMap<>();
832+
arguments.put("sku", skuId);
833+
methodChannelHandler.onMethodCall(
834+
new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
835+
verify(result, times(1)).error(eq("ACTIVITY_UNAVAILABLE"), any(), any());
836+
}
837+
838+
@Test
839+
public void launchPriceChangeConfirmationFlow_withoutSkuQuery_returnsNotFoundError() {
840+
// Set up the sku details
841+
establishConnectedBillingClient(null, null);
842+
String skuId = "foo";
843+
844+
// Call the methodChannelHandler
845+
HashMap<String, Object> arguments = new HashMap<>();
846+
arguments.put("sku", skuId);
847+
methodChannelHandler.onMethodCall(
848+
new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
849+
verify(result, times(1)).error(eq("NOT_FOUND"), contains("sku"), any());
850+
}
851+
852+
@Test
853+
public void launchPriceChangeConfirmationFlow_withoutBillingClient_returnsUnavailableError() {
854+
// Set up the sku details
855+
String skuId = "foo";
856+
857+
// Call the methodChannelHandler
858+
HashMap<String, Object> arguments = new HashMap<>();
859+
arguments.put("sku", skuId);
860+
methodChannelHandler.onMethodCall(
861+
new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
862+
verify(result, times(1)).error(eq("UNAVAILABLE"), contains("BillingClient"), any());
863+
}
864+
771865
private ArgumentCaptor<BillingClientStateListener> mockStartConnection() {
772866
Map<String, Object> arguments = new HashMap<>();
773867
arguments.put("handle", 1);

packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ const bool _kAutoConsume = true;
3131

3232
const String _kConsumableId = 'consumable';
3333
const String _kUpgradeId = 'upgrade';
34-
const String _kSilverSubscriptionId = 'subscription_silver';
35-
const String _kGoldSubscriptionId = 'subscription_gold';
34+
const String _kSilverSubscriptionId = 'subscription_silver1';
35+
const String _kGoldSubscriptionId = 'subscription_gold1';
3636
const List<String> _kProductIds = <String>[
3737
_kConsumableId,
3838
_kUpgradeId,
@@ -251,7 +251,21 @@ class _MyAppState extends State<_MyApp> {
251251
productDetails.description,
252252
),
253253
trailing: previousPurchase != null
254-
? Icon(Icons.check)
254+
? IconButton(
255+
onPressed: () {
256+
final InAppPurchaseAndroidPlatformAddition addition =
257+
InAppPurchasePlatformAddition.instance
258+
as InAppPurchaseAndroidPlatformAddition;
259+
var skuDetails =
260+
(productDetails as GooglePlayProductDetails)
261+
.skuDetails;
262+
addition
263+
.launchPriceChangeConfirmationFlow(
264+
sku: skuDetails.sku)
265+
.then((value) => print(
266+
"confirmationResponse: ${value.responseCode}"));
267+
},
268+
icon: Icon(Icons.upgrade))
255269
: TextButton(
256270
child: Text(productDetails.price),
257271
style: TextButton.styleFrom(

packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,26 @@ class BillingClient {
311311
return result ?? false;
312312
}
313313

314+
/// Initiates a flow to confirm the change of price for an item subscribed by the user.
315+
///
316+
/// When the price of a user subscribed item has changed, launch this flow to take users to
317+
/// a screen with price change information. User can confirm the new price or cancel the flow.
318+
///
319+
/// The skuDetails needs to have already been fetched in a [querySkuDetails]
320+
/// call.
321+
Future<BillingResultWrapper> launchPriceChangeConfirmationFlow(
322+
{required String sku}) async {
323+
assert(sku != null);
324+
final Map<String, dynamic> arguments = <String, dynamic>{
325+
'sku': sku,
326+
};
327+
return BillingResultWrapper.fromJson((await channel.invokeMapMethod<String,
328+
dynamic>(
329+
'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)',
330+
arguments)) ??
331+
<String, dynamic>{});
332+
}
333+
314334
/// The method call handler for [channel].
315335
@visibleForTesting
316336
Future<void> callHandler(MethodCall call) async {

packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,16 @@ class InAppPurchaseAndroidPlatformAddition
141141
Future<bool> isFeatureSupported(BillingClientFeature feature) async {
142142
return _billingClient.isFeatureSupported(feature);
143143
}
144+
145+
/// Initiates a flow to confirm the change of price for an item subscribed by the user.
146+
///
147+
/// When the price of a user subscribed item has changed, launch this flow to take users to
148+
/// a screen with price change information. User can confirm the new price or cancel the flow.
149+
///
150+
/// The skuDetails needs to have already been fetched in a
151+
/// [InAppPurchaseAndroidPlatform.queryProductDetails] call.
152+
Future<BillingResultWrapper> launchPriceChangeConfirmationFlow(
153+
{required String sku}) {
154+
return _billingClient.launchPriceChangeConfirmationFlow(sku: sku);
155+
}
144156
}

packages/in_app_purchase/in_app_purchase_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: in_app_purchase_android
22
description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs.
33
repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
5-
version: 0.1.3+1
5+
version: 0.1.4
66

77
environment:
88
sdk: ">=2.12.0 <3.0.0"

packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,44 @@ void main() {
574574
expect(arguments['feature'], equals('subscriptions'));
575575
});
576576
});
577+
578+
group('launchPriceChangeConfirmationFlow', () {
579+
const String launchPriceChangeConfirmationFlowMethodName =
580+
'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)';
581+
582+
final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper(
583+
responseCode: BillingResponse.ok,
584+
debugMessage: 'dummy message',
585+
);
586+
587+
test('serializes and deserializes data', () async {
588+
stubPlatform.addResponse(
589+
name: launchPriceChangeConfirmationFlowMethodName,
590+
value:
591+
buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
592+
);
593+
594+
expect(
595+
await billingClient.launchPriceChangeConfirmationFlow(
596+
sku: dummySkuDetails.sku,
597+
),
598+
equals(expectedBillingResultPriceChangeConfirmation),
599+
);
600+
});
601+
602+
test('passes sku to launchPriceChangeConfirmationFlow', () async {
603+
stubPlatform.addResponse(
604+
name: launchPriceChangeConfirmationFlowMethodName,
605+
value:
606+
buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
607+
);
608+
await billingClient.launchPriceChangeConfirmationFlow(
609+
sku: dummySkuDetails.sku,
610+
);
611+
final MethodCall call = stubPlatform
612+
.previousCallMatching(launchPriceChangeConfirmationFlowMethodName);
613+
expect(call.arguments,
614+
equals(<dynamic, dynamic>{'sku': dummySkuDetails.sku}));
615+
});
616+
});
577617
}

packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,44 @@ void main() {
171171
expect(arguments['feature'], equals('subscriptions'));
172172
});
173173
});
174+
175+
group('launchPriceChangeConfirmationFlow', () {
176+
const String launchPriceChangeConfirmationFlowMethodName =
177+
'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)';
178+
const dummySku = 'sku';
179+
180+
final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper(
181+
responseCode: BillingResponse.ok,
182+
debugMessage: 'dummy message',
183+
);
184+
185+
test('serializes and deserializes data', () async {
186+
stubPlatform.addResponse(
187+
name: launchPriceChangeConfirmationFlowMethodName,
188+
value:
189+
buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
190+
);
191+
192+
expect(
193+
await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow(
194+
sku: dummySku,
195+
),
196+
equals(expectedBillingResultPriceChangeConfirmation),
197+
);
198+
});
199+
200+
test('passes sku to launchPriceChangeConfirmationFlow', () async {
201+
stubPlatform.addResponse(
202+
name: launchPriceChangeConfirmationFlowMethodName,
203+
value:
204+
buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
205+
);
206+
await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow(
207+
sku: dummySku,
208+
);
209+
final MethodCall call = stubPlatform
210+
.previousCallMatching(launchPriceChangeConfirmationFlowMethodName);
211+
expect(call.arguments, equals(<dynamic, dynamic>{'sku': dummySku}));
212+
});
213+
});
174214
}

0 commit comments

Comments
 (0)