Skip to content
Open
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
import static io.flutter.plugins.inapppurchase.Translator.fromInAppMessageResult;
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
Expand All @@ -31,6 +32,7 @@
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.GetBillingConfigParams;
import com.android.billingclient.api.InAppMessageParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchaseHistoryParams;
Expand Down Expand Up @@ -176,6 +178,29 @@ public void isAlternativeBillingOnlyAvailableAsync(
}
}

@Override
public void showInAppMessages(@NonNull Result<Messages.PlatformInAppMessageResult> result) {
if (billingClient == null) {
result.error(getNullBillingClientError());
return;
}
if (activity == null) {
result.error(new FlutterError(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null));
return;
}
try {
InAppMessageParams params =
InAppMessageParams.newBuilder()
.addInAppMessageCategoryToShow(
InAppMessageParams.InAppMessageCategoryId.TRANSACTIONAL)
.build();
billingClient.showInAppMessages(
activity, params, billingResult -> result.success(fromInAppMessageResult(billingResult)));
} catch (RuntimeException e) {
result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e)));
}
}

@Override
public void getBillingConfigAsync(
@NonNull Result<Messages.PlatformBillingConfigResponse> result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.android.billingclient.api.BillingConfig;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.InAppMessageResult;
import com.android.billingclient.api.PendingPurchasesParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
Expand All @@ -25,6 +26,8 @@
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingConfigResponse;
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResponse;
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult;
import io.flutter.plugins.inapppurchase.Messages.PlatformInAppMessageResponse;
import io.flutter.plugins.inapppurchase.Messages.PlatformInAppMessageResult;
import io.flutter.plugins.inapppurchase.Messages.PlatformOneTimePurchaseOfferDetails;
import io.flutter.plugins.inapppurchase.Messages.PlatformPendingPurchaseUpdate;
import io.flutter.plugins.inapppurchase.Messages.PlatformPricingPhase;
Expand Down Expand Up @@ -298,6 +301,25 @@ static PlatformPurchaseState toPlatformPurchaseState(int state) {
return serialized;
}

static @NonNull PlatformInAppMessageResult fromInAppMessageResult(
@NonNull InAppMessageResult inAppMessageResult) {
return new PlatformInAppMessageResult.Builder()
.setResponseCode(fromInAppMessageResponseCode(inAppMessageResult.getResponseCode()))
.setPurchaseToken(inAppMessageResult.getPurchaseToken())
.build();
}

static @NonNull PlatformInAppMessageResponse fromInAppMessageResponseCode(
@InAppMessageResult.InAppMessageResponseCode int inAppMessageResponseCode) {
switch (inAppMessageResponseCode) {
case InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED:
return PlatformInAppMessageResponse.SUBSCRIPTION_STATUS_UPDATED;
case InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED:
return PlatformInAppMessageResponse.NO_ACTION_NEEDED;
}
return PlatformInAppMessageResponse.NO_ACTION_NEEDED;
}
Comment on lines +312 to +321

Choose a reason for hiding this comment

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

medium

For better maintainability and to explicitly handle any future InAppMessageResponseCode values, it's good practice to include a default case in your switch statement. This makes the code's intent clearer and prevents new values from being silently treated as NO_ACTION_NEEDED.

Suggested change
static @NonNull PlatformInAppMessageResponse fromInAppMessageResponseCode(
@InAppMessageResult.InAppMessageResponseCode int inAppMessageResponseCode) {
switch (inAppMessageResponseCode) {
case InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED:
return PlatformInAppMessageResponse.SUBSCRIPTION_STATUS_UPDATED;
case InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED:
return PlatformInAppMessageResponse.NO_ACTION_NEEDED;
}
return PlatformInAppMessageResponse.NO_ACTION_NEEDED;
}
static @NonNull PlatformInAppMessageResponse fromInAppMessageResponseCode(
@InAppMessageResult.InAppMessageResponseCode int inAppMessageResponseCode) {
switch (inAppMessageResponseCode) {
case InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED:
return PlatformInAppMessageResponse.SUBSCRIPTION_STATUS_UPDATED;
case InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED:
default:
return PlatformInAppMessageResponse.NO_ACTION_NEEDED;
}
}


static @NonNull PlatformBillingResult fromBillingResult(@NonNull BillingResult billingResult) {
return new PlatformBillingResult.Builder()
.setResponseCode(fromBillingResponseCode(billingResult.getResponseCode()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingResponseCode;
import static io.flutter.plugins.inapppurchase.Translator.fromInAppMessageResponseCode;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
Expand Down Expand Up @@ -45,6 +46,8 @@
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.GetBillingConfigParams;
import com.android.billingclient.api.InAppMessageResponseListener;
import com.android.billingclient.api.InAppMessageResult;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
Expand All @@ -63,6 +66,7 @@
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingConfigResponse;
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams;
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult;
import io.flutter.plugins.inapppurchase.Messages.PlatformInAppMessageResult;
import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse;
import io.flutter.plugins.inapppurchase.Messages.PlatformProductType;
import io.flutter.plugins.inapppurchase.Messages.PlatformPurchaseHistoryResponse;
Expand Down Expand Up @@ -96,6 +100,7 @@ public class MethodCallHandlerTest {
Messages.Result<Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse>
platformAlternativeBillingOnlyReportingDetailsResult;

@Spy Messages.Result<Messages.PlatformInAppMessageResult> platformInAppMessageResult;
@Spy Messages.Result<Messages.PlatformBillingConfigResponse> platformBillingConfigResult;
@Spy Messages.Result<PlatformBillingResult> platformBillingResult;
@Spy Messages.Result<PlatformProductDetailsResponse> platformProductDetailsResult;
Expand Down Expand Up @@ -527,6 +532,62 @@ public void showAlternativeBillingOnlyInformationDialog_NullActivity() {
.contains("Not attempting to show dialog"));
}

@Test
public void showInAppMessagesSuccess() {
mockStartConnection();
ArgumentCaptor<InAppMessageResponseListener> listenerCaptor =
ArgumentCaptor.forClass(InAppMessageResponseListener.class);
BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK);
InAppMessageResult inAppMessageResult =
buildInAppMessageResult(
InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED);

when(mockBillingClient.showInAppMessages(eq(activity), any(), listenerCaptor.capture()))
.thenReturn(billingResult);

methodChannelHandler.showInAppMessages(platformInAppMessageResult);
listenerCaptor.getValue().onInAppMessageResponse(inAppMessageResult);

ArgumentCaptor<PlatformInAppMessageResult> resultCaptor =
ArgumentCaptor.forClass(PlatformInAppMessageResult.class);
verify(platformInAppMessageResult, times(1)).success(resultCaptor.capture());
assertEquals(
resultCaptor.getValue().getResponseCode(),
fromInAppMessageResponseCode(inAppMessageResult.getResponseCode()));
assertEquals(resultCaptor.getValue().getPurchaseToken(), inAppMessageResult.getPurchaseToken());
verify(platformInAppMessageResult, never()).error(any());
}

@Test
public void showInAppMessages_serviceDisconnected() {
methodChannelHandler.showInAppMessages(platformInAppMessageResult);

// Assert that the async call returns an error result.
verify(platformInAppMessageResult, never()).success(any());
ArgumentCaptor<FlutterError> errorCaptor = ArgumentCaptor.forClass(FlutterError.class);
verify(platformInAppMessageResult, times(1)).error(errorCaptor.capture());
assertEquals("UNAVAILABLE", errorCaptor.getValue().code);
assertTrue(
Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient"));
}

@Test
public void showInAppMessages_NullActivity() {
mockStartConnection();
methodChannelHandler.setActivity(null);

methodChannelHandler.showInAppMessages(platformInAppMessageResult);

// Assert that the async call returns an error result.
verify(platformInAppMessageResult, never()).success(any());
ArgumentCaptor<FlutterError> errorCaptor = ArgumentCaptor.forClass(FlutterError.class);
verify(platformInAppMessageResult, times(1)).error(errorCaptor.capture());
assertEquals(ACTIVITY_UNAVAILABLE, errorCaptor.getValue().code);
assertTrue(
Objects.requireNonNull(errorCaptor.getValue().getMessage())
.contains("Not attempting to show dialog"));
}

@Test
public void endConnection() {
// Set up a connected BillingClient instance
Expand Down Expand Up @@ -1252,6 +1313,10 @@ private BillingResult buildBillingResult(int responseCode) {
.build();
}

private InAppMessageResult buildInAppMessageResult(int responseCode) {
return new InAppMessageResult(responseCode, "dummy purchase token");
}

private void assertResultsMatch(PlatformBillingResult pigeonResult, BillingResult nativeResult) {
assertEquals(
pigeonResult.getResponseCode(), fromBillingResponseCode(nativeResult.getResponseCode()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class _MyAppState extends State<_MyApp> {
String _isAlternativeBillingOnlyAvailableResponseCode = '';
String _showAlternativeBillingOnlyDialogResponseCode = '';
String _alternativeBillingOnlyReportingDetailsToken = '';
String _showInAppMessagesResponseCode = '';
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
Expand Down Expand Up @@ -274,6 +275,15 @@ class _MyAppState extends State<_MyApp> {
subtitle: Text(_alternativeBillingOnlyReportingDetailsToken),
),
);
entries.add(
ListTile(
title: Text(
'showInAppMessages response code',
style: TextStyle(color: ThemeData.light().colorScheme.primary),
),
subtitle: Text(_showInAppMessagesResponseCode),
),
);

final List<Widget> buttons = <ListTile>[];
buttons.add(
Expand Down Expand Up @@ -374,6 +384,25 @@ class _MyAppState extends State<_MyApp> {
),
),
);
buttons.add(
ListTile(
title: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
foregroundColor: Colors.white,
),
onPressed: () {
final addition =
InAppPurchasePlatformAddition.instance!
as InAppPurchaseAndroidPlatformAddition;
unawaited(
deliverShowInAppMessagesResult(addition.showInAppMessages()),
);
},
child: const Text('showInAppMessages'),
),
),
);
return Card(
child: Column(
children: <Widget>[
Expand Down Expand Up @@ -597,6 +626,15 @@ class _MyAppState extends State<_MyApp> {
});
}

Future<void> deliverShowInAppMessagesResult(
Future<InAppMessageResultWrapper> inAppMessageResult,
) async {
final InAppMessageResultWrapper wrapper = await inAppMessageResult;
setState(() {
_showInAppMessagesResponseCode = wrapper.responseCode.name;
});
}

Future<void> deliverProduct(PurchaseDetails purchaseDetails) async {
// IMPORTANT!! Always verify purchase details before delivering the product.
if (purchaseDetails.productID == _kConsumableId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export 'src/billing_client_wrappers/alternative_billing_only_reporting_details_w
export 'src/billing_client_wrappers/billing_client_manager.dart';
export 'src/billing_client_wrappers/billing_client_wrapper.dart';
export 'src/billing_client_wrappers/billing_response_wrapper.dart';
export 'src/billing_client_wrappers/in_app_message_wrapper.dart';
export 'src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.dart';
export 'src/billing_client_wrappers/product_details_wrapper.dart';
export 'src/billing_client_wrappers/product_wrapper.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import '../../billing_client_wrappers.dart';
import '../messages.g.dart';
import '../pigeon_converters.dart';
import 'billing_config_wrapper.dart';
import 'in_app_message_wrapper.dart';
import 'pending_purchases_params_wrapper.dart';

/// Callback triggered by Play in response to purchase activity.
Expand Down Expand Up @@ -347,6 +348,16 @@ class BillingClient {
await _hostApi.createAlternativeBillingOnlyReportingDetailsAsync(),
);
}

/// Overlays billing related messages on top of the calling app.
//
// For example, show a message to inform users that their subscription payment
// has been declined and provide options to take them to fix their payment method.
Future<InAppMessageResultWrapper> showInAppMessages() async {
return inAppMessageResultWrapperFromPlatform(
await _hostApi.showInAppMessages(),
);
}
}

/// Implementation of InAppPurchaseCallbackApi, for use by [BillingClient].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/foundation.dart';

/// Response code for the in-app messaging API call.
enum InAppMessageResponse {
/// The flow has finished and there is no action needed from developers.
///
/// Note: The API callback won't indicate whether message is dismissed by the
/// user or there is no message available to the user.
noActionNeeded,

/// The subscription status changed.
///
/// For example, a subscription has been rec-
/// overed from a suspended state. Developers should expect the purchase token
/// to be returned with this response code and use the purchase token with the
/// Google Play Developer API.
subscriptionStatusUpdated,
}

/// Results related to in-app messaging.
///
/// Wraps [`com.android.billingclient.api.InAppMessageResult`](https://developer.android.com/reference/com/android/billingclient/api/InAppMessageResult).
@immutable
class InAppMessageResultWrapper {
/// Creates a [InAppMessageResultWrapper]
const InAppMessageResultWrapper({
required this.responseCode,
this.purchaseToken,
});

/// Returns response code for the in-app messaging API call.
final InAppMessageResponse responseCode;

/// Returns token that identifies the purchase to be acknowledged, if any.
final String? purchaseToken;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InAppMessageResultWrapper &&
runtimeType == other.runtimeType &&
responseCode == other.responseCode &&
purchaseToken == other.purchaseToken;

@override
int get hashCode => Object.hash(responseCode, purchaseToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ class InAppPurchaseAndroidPlatformAddition
return wrapper;
}

/// Overlays billing related messages on top of the calling app.
///
/// For example, show a message to inform users that their subscription payment
/// has been declined and provide options to take them to fix their payment method.
/// See: https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showInAppMessages(android.app.Activity,com.android.billingclient.api.InAppMessageParams,com.android.billingclient.api.InAppMessageResponseListener)
Future<InAppMessageResultWrapper> showInAppMessages() async {
final InAppMessageResultWrapper wrapper = await _billingClientManager
.runWithClientNonRetryable(
(BillingClient client) => client.showInAppMessages(),
);
return wrapper;
}

/// Disconnects, sets AlternativeBillingOnly to true, and reconnects to
/// the [BillingClient].
///
Expand Down
Loading