diff --git a/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart b/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart index 1656338f..e0a2f44d 100644 --- a/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart +++ b/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart @@ -386,6 +386,8 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { subscriptionOffers; final gentype.DeveloperBillingOptionParamsAndroid? developerBillingOption; + final gentype.SubscriptionProductReplacementParamsAndroid? + subscriptionProductReplacementParams; if (androidProps is gentype.RequestPurchaseAndroidProps) { skus = androidProps.skus; @@ -397,6 +399,7 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { offerToken = androidProps.offerToken; subscriptionOffers = null; developerBillingOption = androidProps.developerBillingOption; + subscriptionProductReplacementParams = null; } else if (androidProps is gentype.RequestSubscriptionAndroidProps) { skus = androidProps.skus; @@ -408,6 +411,8 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { offerToken = null; // Subscriptions don't use offerToken subscriptionOffers = androidProps.subscriptionOffers; developerBillingOption = androidProps.developerBillingOption; + subscriptionProductReplacementParams = + androidProps.subscriptionProductReplacementParams; } else { throw PurchaseError( code: gentype.ErrorCode.DeveloperError, @@ -468,6 +473,13 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { developerBillingOption.toJson(); } + // Add subscriptionProductReplacementParams for item-level + // replacement (Billing Library 8.1.0+) + if (subscriptionProductReplacementParams != null) { + payload['subscriptionProductReplacementParams'] = + subscriptionProductReplacementParams.toJson(); + } + await _channel.invokeMethod('requestPurchase', payload); return null; } diff --git a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart index 14e23e9c..1fef951e 100644 --- a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart +++ b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart @@ -658,6 +658,62 @@ void main() { expect(payload.containsKey('subscriptionOffers'), isFalse); }); + test( + 'forwards subscriptionProductReplacementParams on Android subscription', + () async { + final calls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall call) async { + calls.add(call); + switch (call.method) { + case 'initConnection': + return true; + case 'requestPurchase': + return null; + } + return null; + }); + + final iap = FlutterInappPurchase.private( + FakePlatform(operatingSystem: 'android'), + ); + + await iap.initConnection(); + + const props = types.RequestPurchaseProps.subs(( + apple: null, + google: types.RequestSubscriptionAndroidProps( + skus: ['sub.premium'], + subscriptionProductReplacementParams: + types.SubscriptionProductReplacementParamsAndroid( + oldProductId: 'sub.premium', + replacementMode: + types.SubscriptionReplacementModeAndroid.ChargeFullPrice, + ), + ), + useAlternativeBilling: null, + )); + + await iap.requestPurchase(props); + + final requestCall = calls.singleWhere( + (MethodCall c) => c.method == 'requestPurchase', + ); + final payload = Map.from( + requestCall.arguments as Map, + ); + + expect(payload.containsKey('subscriptionProductReplacementParams'), + isTrue); + final replacement = Map.from( + payload['subscriptionProductReplacementParams'] + as Map, + ); + expect(replacement['oldProductId'], 'sub.premium'); + expect(replacement['replacementMode'], 'charge-full-price'); + }, + ); + test( 'sends developerBillingOption for External Payments on Android', () async {