diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index fa1135fed036..aba22ed2b3bc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -95,6 +95,31 @@ public enum PlatformBillingResponse { } } + /** Response code for the in-app messaging API call. */ + public enum PlatformInAppMessageResponse { + /** + * 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. + */ + NO_ACTION_NEEDED(0), + /** + * 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. + */ + SUBSCRIPTION_STATUS_UPDATED(1); + + final int index; + + PlatformInAppMessageResponse(final int index) { + this.index = index; + } + } + public enum PlatformReplacementMode { UNKNOWN_REPLACEMENT_MODE(0), WITH_TIME_PRORATION(1), @@ -1025,6 +1050,102 @@ ArrayList toList() { } } + /** + * Results related to in-app messaging. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformInAppMessageResult { + /** Returns response code for the in-app messaging API call. */ + private @NonNull PlatformInAppMessageResponse responseCode; + + public @NonNull PlatformInAppMessageResponse getResponseCode() { + return responseCode; + } + + public void setResponseCode(@NonNull PlatformInAppMessageResponse setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"responseCode\" is null."); + } + this.responseCode = setterArg; + } + + /** Returns token that identifies the purchase to be acknowledged, if any. */ + private @Nullable String purchaseToken; + + public @Nullable String getPurchaseToken() { + return purchaseToken; + } + + public void setPurchaseToken(@Nullable String setterArg) { + this.purchaseToken = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformInAppMessageResult() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformInAppMessageResult that = (PlatformInAppMessageResult) o; + return responseCode.equals(that.responseCode) + && Objects.equals(purchaseToken, that.purchaseToken); + } + + @Override + public int hashCode() { + return Objects.hash(responseCode, purchaseToken); + } + + public static final class Builder { + + private @Nullable PlatformInAppMessageResponse responseCode; + + @CanIgnoreReturnValue + public @NonNull Builder setResponseCode(@NonNull PlatformInAppMessageResponse setterArg) { + this.responseCode = setterArg; + return this; + } + + private @Nullable String purchaseToken; + + @CanIgnoreReturnValue + public @NonNull Builder setPurchaseToken(@Nullable String setterArg) { + this.purchaseToken = setterArg; + return this; + } + + public @NonNull PlatformInAppMessageResult build() { + PlatformInAppMessageResult pigeonReturn = new PlatformInAppMessageResult(); + pigeonReturn.setResponseCode(responseCode); + pigeonReturn.setPurchaseToken(purchaseToken); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(responseCode); + toListResult.add(purchaseToken); + return toListResult; + } + + static @NonNull PlatformInAppMessageResult fromList(@NonNull ArrayList pigeonVar_list) { + PlatformInAppMessageResult pigeonResult = new PlatformInAppMessageResult(); + Object responseCode = pigeonVar_list.get(0); + pigeonResult.setResponseCode((PlatformInAppMessageResponse) responseCode); + Object purchaseToken = pigeonVar_list.get(1); + pigeonResult.setPurchaseToken((String) purchaseToken); + return pigeonResult; + } + } + /** * Pigeon version of BillingConfigWrapper, which contains the components of the Java * BillingConfigResponseListener callback. @@ -3126,80 +3247,89 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { Object value = readValue(buffer); return value == null ? null - : PlatformReplacementMode.values()[((Long) value).intValue()]; + : PlatformInAppMessageResponse.values()[((Long) value).intValue()]; } case (byte) 131: { Object value = readValue(buffer); - return value == null ? null : PlatformProductType.values()[((Long) value).intValue()]; + return value == null + ? null + : PlatformReplacementMode.values()[((Long) value).intValue()]; } case (byte) 132: + { + Object value = readValue(buffer); + return value == null ? null : PlatformProductType.values()[((Long) value).intValue()]; + } + case (byte) 133: { Object value = readValue(buffer); return value == null ? null : PlatformBillingChoiceMode.values()[((Long) value).intValue()]; } - case (byte) 133: + case (byte) 134: { Object value = readValue(buffer); return value == null ? null : PlatformBillingClientFeature.values()[((Long) value).intValue()]; } - case (byte) 134: + case (byte) 135: { Object value = readValue(buffer); return value == null ? null : PlatformPurchaseState.values()[((Long) value).intValue()]; } - case (byte) 135: + case (byte) 136: { Object value = readValue(buffer); return value == null ? null : PlatformRecurrenceMode.values()[((Long) value).intValue()]; } - case (byte) 136: - return PlatformQueryProduct.fromList((ArrayList) readValue(buffer)); case (byte) 137: - return PlatformAccountIdentifiers.fromList((ArrayList) readValue(buffer)); + return PlatformQueryProduct.fromList((ArrayList) readValue(buffer)); case (byte) 138: - return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + return PlatformAccountIdentifiers.fromList((ArrayList) readValue(buffer)); case (byte) 139: + return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + case (byte) 140: return PlatformOneTimePurchaseOfferDetails.fromList( (ArrayList) readValue(buffer)); - case (byte) 140: - return PlatformProductDetails.fromList((ArrayList) readValue(buffer)); case (byte) 141: - return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); + return PlatformProductDetails.fromList((ArrayList) readValue(buffer)); case (byte) 142: + return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 143: return PlatformAlternativeBillingOnlyReportingDetailsResponse.fromList( (ArrayList) readValue(buffer)); - case (byte) 143: - return PlatformBillingConfigResponse.fromList((ArrayList) readValue(buffer)); case (byte) 144: - return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); + return PlatformInAppMessageResult.fromList((ArrayList) readValue(buffer)); case (byte) 145: - return PlatformPricingPhase.fromList((ArrayList) readValue(buffer)); + return PlatformBillingConfigResponse.fromList((ArrayList) readValue(buffer)); case (byte) 146: - return PlatformPurchase.fromList((ArrayList) readValue(buffer)); + return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); case (byte) 147: - return PlatformPendingPurchaseUpdate.fromList((ArrayList) readValue(buffer)); + return PlatformPricingPhase.fromList((ArrayList) readValue(buffer)); case (byte) 148: - return PlatformPurchaseHistoryRecord.fromList((ArrayList) readValue(buffer)); + return PlatformPurchase.fromList((ArrayList) readValue(buffer)); case (byte) 149: - return PlatformPurchaseHistoryResponse.fromList((ArrayList) readValue(buffer)); + return PlatformPendingPurchaseUpdate.fromList((ArrayList) readValue(buffer)); case (byte) 150: - return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); + return PlatformPurchaseHistoryRecord.fromList((ArrayList) readValue(buffer)); case (byte) 151: - return PlatformSubscriptionOfferDetails.fromList((ArrayList) readValue(buffer)); + return PlatformPurchaseHistoryResponse.fromList((ArrayList) readValue(buffer)); case (byte) 152: - return PlatformUserChoiceDetails.fromList((ArrayList) readValue(buffer)); + return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); case (byte) 153: - return PlatformUserChoiceProduct.fromList((ArrayList) readValue(buffer)); + return PlatformSubscriptionOfferDetails.fromList((ArrayList) readValue(buffer)); case (byte) 154: - return PlatformInstallmentPlanDetails.fromList((ArrayList) readValue(buffer)); + return PlatformUserChoiceDetails.fromList((ArrayList) readValue(buffer)); case (byte) 155: + return PlatformUserChoiceProduct.fromList((ArrayList) readValue(buffer)); + case (byte) 156: + return PlatformInstallmentPlanDetails.fromList((ArrayList) readValue(buffer)); + case (byte) 157: return PlatformPendingPurchasesParams.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -3211,84 +3341,90 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof PlatformBillingResponse) { stream.write(129); writeValue(stream, value == null ? null : ((PlatformBillingResponse) value).index); - } else if (value instanceof PlatformReplacementMode) { + } else if (value instanceof PlatformInAppMessageResponse) { stream.write(130); + writeValue(stream, value == null ? null : ((PlatformInAppMessageResponse) value).index); + } else if (value instanceof PlatformReplacementMode) { + stream.write(131); writeValue(stream, value == null ? null : ((PlatformReplacementMode) value).index); } else if (value instanceof PlatformProductType) { - stream.write(131); + stream.write(132); writeValue(stream, value == null ? null : ((PlatformProductType) value).index); } else if (value instanceof PlatformBillingChoiceMode) { - stream.write(132); + stream.write(133); writeValue(stream, value == null ? null : ((PlatformBillingChoiceMode) value).index); } else if (value instanceof PlatformBillingClientFeature) { - stream.write(133); + stream.write(134); writeValue(stream, value == null ? null : ((PlatformBillingClientFeature) value).index); } else if (value instanceof PlatformPurchaseState) { - stream.write(134); + stream.write(135); writeValue(stream, value == null ? null : ((PlatformPurchaseState) value).index); } else if (value instanceof PlatformRecurrenceMode) { - stream.write(135); + stream.write(136); writeValue(stream, value == null ? null : ((PlatformRecurrenceMode) value).index); } else if (value instanceof PlatformQueryProduct) { - stream.write(136); + stream.write(137); writeValue(stream, ((PlatformQueryProduct) value).toList()); } else if (value instanceof PlatformAccountIdentifiers) { - stream.write(137); + stream.write(138); writeValue(stream, ((PlatformAccountIdentifiers) value).toList()); } else if (value instanceof PlatformBillingResult) { - stream.write(138); + stream.write(139); writeValue(stream, ((PlatformBillingResult) value).toList()); } else if (value instanceof PlatformOneTimePurchaseOfferDetails) { - stream.write(139); + stream.write(140); writeValue(stream, ((PlatformOneTimePurchaseOfferDetails) value).toList()); } else if (value instanceof PlatformProductDetails) { - stream.write(140); + stream.write(141); writeValue(stream, ((PlatformProductDetails) value).toList()); } else if (value instanceof PlatformProductDetailsResponse) { - stream.write(141); + stream.write(142); writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); } else if (value instanceof PlatformAlternativeBillingOnlyReportingDetailsResponse) { - stream.write(142); + stream.write(143); writeValue( stream, ((PlatformAlternativeBillingOnlyReportingDetailsResponse) value).toList()); + } else if (value instanceof PlatformInAppMessageResult) { + stream.write(144); + writeValue(stream, ((PlatformInAppMessageResult) value).toList()); } else if (value instanceof PlatformBillingConfigResponse) { - stream.write(143); + stream.write(145); writeValue(stream, ((PlatformBillingConfigResponse) value).toList()); } else if (value instanceof PlatformBillingFlowParams) { - stream.write(144); + stream.write(146); writeValue(stream, ((PlatformBillingFlowParams) value).toList()); } else if (value instanceof PlatformPricingPhase) { - stream.write(145); + stream.write(147); writeValue(stream, ((PlatformPricingPhase) value).toList()); } else if (value instanceof PlatformPurchase) { - stream.write(146); + stream.write(148); writeValue(stream, ((PlatformPurchase) value).toList()); } else if (value instanceof PlatformPendingPurchaseUpdate) { - stream.write(147); + stream.write(149); writeValue(stream, ((PlatformPendingPurchaseUpdate) value).toList()); } else if (value instanceof PlatformPurchaseHistoryRecord) { - stream.write(148); + stream.write(150); writeValue(stream, ((PlatformPurchaseHistoryRecord) value).toList()); } else if (value instanceof PlatformPurchaseHistoryResponse) { - stream.write(149); + stream.write(151); writeValue(stream, ((PlatformPurchaseHistoryResponse) value).toList()); } else if (value instanceof PlatformPurchasesResponse) { - stream.write(150); + stream.write(152); writeValue(stream, ((PlatformPurchasesResponse) value).toList()); } else if (value instanceof PlatformSubscriptionOfferDetails) { - stream.write(151); + stream.write(153); writeValue(stream, ((PlatformSubscriptionOfferDetails) value).toList()); } else if (value instanceof PlatformUserChoiceDetails) { - stream.write(152); + stream.write(154); writeValue(stream, ((PlatformUserChoiceDetails) value).toList()); } else if (value instanceof PlatformUserChoiceProduct) { - stream.write(153); + stream.write(155); writeValue(stream, ((PlatformUserChoiceProduct) value).toList()); } else if (value instanceof PlatformInstallmentPlanDetails) { - stream.write(154); + stream.write(156); writeValue(stream, ((PlatformInstallmentPlanDetails) value).toList()); } else if (value instanceof PlatformPendingPurchasesParams) { - stream.write(155); + stream.write(157); writeValue(stream, ((PlatformPendingPurchasesParams) value).toList()); } else { super.writeValue(stream, value); @@ -3380,6 +3516,8 @@ void queryProductDetailsAsync( */ void createAlternativeBillingOnlyReportingDetailsAsync( @NonNull Result result); + /** Wraps BillingClient#showInAppMessages(). */ + void showInAppMessages(@NonNull Result result); /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { @@ -3811,6 +3949,36 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.showInAppMessages" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + Result resultCallback = + new Result() { + public void success(PlatformInAppMessageResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.showInAppMessages(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index d496fb57b323..262b9a85259f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -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; @@ -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; @@ -176,6 +178,29 @@ public void isAlternativeBillingOnlyAvailableAsync( } } + @Override + public void showInAppMessages(@NonNull Result 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 result) { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index a982964764dc..1bfb9c64996d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -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; @@ -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; @@ -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; + } + static @NonNull PlatformBillingResult fromBillingResult(@NonNull BillingResult billingResult) { return new PlatformBillingResult.Builder() .setResponseCode(fromBillingResponseCode(billingResult.getResponseCode())) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index d4cfeda77d0d..723c655d85aa 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -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; @@ -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; @@ -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; @@ -96,6 +100,7 @@ public class MethodCallHandlerTest { Messages.Result platformAlternativeBillingOnlyReportingDetailsResult; + @Spy Messages.Result platformInAppMessageResult; @Spy Messages.Result platformBillingConfigResult; @Spy Messages.Result platformBillingResult; @Spy Messages.Result platformProductDetailsResult; @@ -527,6 +532,62 @@ public void showAlternativeBillingOnlyInformationDialog_NullActivity() { .contains("Not attempting to show dialog")); } + @Test + public void showInAppMessagesSuccess() { + mockStartConnection(); + ArgumentCaptor 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 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 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 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 @@ -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())); diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index 6bb520121d19..bbc860589ce6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -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; @@ -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 buttons = []; buttons.add( @@ -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: [ @@ -597,6 +626,15 @@ class _MyAppState extends State<_MyApp> { }); } + Future deliverShowInAppMessagesResult( + Future inAppMessageResult, + ) async { + final InAppMessageResultWrapper wrapper = await inAppMessageResult; + setState(() { + _showInAppMessagesResponseCode = wrapper.responseCode.name; + }); + } + Future deliverProduct(PurchaseDetails purchaseDetails) async { // IMPORTANT!! Always verify purchase details before delivering the product. if (purchaseDetails.productID == _kConsumableId) { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart index 2224a4e8a543..8d5fda1661b5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart @@ -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'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart index b3df0cf619b9..0924054c0c32 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -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. @@ -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 showInAppMessages() async { + return inAppMessageResultWrapperFromPlatform( + await _hostApi.showInAppMessages(), + ); + } } /// Implementation of InAppPurchaseCallbackApi, for use by [BillingClient]. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/in_app_message_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/in_app_message_wrapper.dart new file mode 100644 index 000000000000..c694a322fc42 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/in_app_message_wrapper.dart @@ -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); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart index e4d27cdb99bc..5dd2e3f9aa35 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart @@ -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 showInAppMessages() async { + final InAppMessageResultWrapper wrapper = await _billingClientManager + .runWithClientNonRetryable( + (BillingClient client) => client.showInAppMessages(), + ); + return wrapper; + } + /// Disconnects, sets AlternativeBillingOnly to true, and reconnects to /// the [BillingClient]. /// diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index cbcdb28231df..dba8c866b9dc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -67,6 +67,23 @@ enum PlatformBillingResponse { networkError, } +/// Response code for the in-app messaging API call. +enum PlatformInAppMessageResponse { + /// 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, +} + enum PlatformReplacementMode { unknownReplacementMode, withTimeProration, @@ -460,6 +477,50 @@ class PlatformAlternativeBillingOnlyReportingDetailsResponse { int get hashCode => Object.hashAll(_toList()); } +/// Results related to in-app messaging. +class PlatformInAppMessageResult { + PlatformInAppMessageResult({required this.responseCode, this.purchaseToken}); + + /// Returns response code for the in-app messaging API call. + PlatformInAppMessageResponse responseCode; + + /// Returns token that identifies the purchase to be acknowledged, if any. + String? purchaseToken; + + List _toList() { + return [responseCode, purchaseToken]; + } + + Object encode() { + return _toList(); + } + + static PlatformInAppMessageResult decode(Object result) { + result as List; + return PlatformInAppMessageResult( + responseCode: result[0]! as PlatformInAppMessageResponse, + purchaseToken: result[1] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformInAppMessageResult || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + /// Pigeon version of BillingConfigWrapper, which contains the components of the /// Java BillingConfigResponseListener callback. class PlatformBillingConfigResponse { @@ -1240,84 +1301,90 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformBillingResponse) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformReplacementMode) { + } else if (value is PlatformInAppMessageResponse) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is PlatformProductType) { + } else if (value is PlatformReplacementMode) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is PlatformBillingChoiceMode) { + } else if (value is PlatformProductType) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is PlatformBillingClientFeature) { + } else if (value is PlatformBillingChoiceMode) { buffer.putUint8(133); writeValue(buffer, value.index); - } else if (value is PlatformPurchaseState) { + } else if (value is PlatformBillingClientFeature) { buffer.putUint8(134); writeValue(buffer, value.index); - } else if (value is PlatformRecurrenceMode) { + } else if (value is PlatformPurchaseState) { buffer.putUint8(135); writeValue(buffer, value.index); - } else if (value is PlatformQueryProduct) { + } else if (value is PlatformRecurrenceMode) { buffer.putUint8(136); + writeValue(buffer, value.index); + } else if (value is PlatformQueryProduct) { + buffer.putUint8(137); writeValue(buffer, value.encode()); } else if (value is PlatformAccountIdentifiers) { - buffer.putUint8(137); + buffer.putUint8(138); writeValue(buffer, value.encode()); } else if (value is PlatformBillingResult) { - buffer.putUint8(138); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is PlatformOneTimePurchaseOfferDetails) { - buffer.putUint8(139); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is PlatformProductDetails) { - buffer.putUint8(140); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is PlatformProductDetailsResponse) { - buffer.putUint8(141); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is PlatformAlternativeBillingOnlyReportingDetailsResponse) { - buffer.putUint8(142); + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is PlatformInAppMessageResult) { + buffer.putUint8(144); writeValue(buffer, value.encode()); } else if (value is PlatformBillingConfigResponse) { - buffer.putUint8(143); + buffer.putUint8(145); writeValue(buffer, value.encode()); } else if (value is PlatformBillingFlowParams) { - buffer.putUint8(144); + buffer.putUint8(146); writeValue(buffer, value.encode()); } else if (value is PlatformPricingPhase) { - buffer.putUint8(145); + buffer.putUint8(147); writeValue(buffer, value.encode()); } else if (value is PlatformPurchase) { - buffer.putUint8(146); + buffer.putUint8(148); writeValue(buffer, value.encode()); } else if (value is PlatformPendingPurchaseUpdate) { - buffer.putUint8(147); + buffer.putUint8(149); writeValue(buffer, value.encode()); } else if (value is PlatformPurchaseHistoryRecord) { - buffer.putUint8(148); + buffer.putUint8(150); writeValue(buffer, value.encode()); } else if (value is PlatformPurchaseHistoryResponse) { - buffer.putUint8(149); + buffer.putUint8(151); writeValue(buffer, value.encode()); } else if (value is PlatformPurchasesResponse) { - buffer.putUint8(150); + buffer.putUint8(152); writeValue(buffer, value.encode()); } else if (value is PlatformSubscriptionOfferDetails) { - buffer.putUint8(151); + buffer.putUint8(153); writeValue(buffer, value.encode()); } else if (value is PlatformUserChoiceDetails) { - buffer.putUint8(152); + buffer.putUint8(154); writeValue(buffer, value.encode()); } else if (value is PlatformUserChoiceProduct) { - buffer.putUint8(153); + buffer.putUint8(155); writeValue(buffer, value.encode()); } else if (value is PlatformInstallmentPlanDetails) { - buffer.putUint8(154); + buffer.putUint8(156); writeValue(buffer, value.encode()); } else if (value is PlatformPendingPurchasesParams) { - buffer.putUint8(155); + buffer.putUint8(157); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -1332,65 +1399,72 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : PlatformBillingResponse.values[value]; case 130: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformReplacementMode.values[value]; + return value == null + ? null + : PlatformInAppMessageResponse.values[value]; case 131: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformProductType.values[value]; + return value == null ? null : PlatformReplacementMode.values[value]; case 132: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformBillingChoiceMode.values[value]; + return value == null ? null : PlatformProductType.values[value]; case 133: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformBillingChoiceMode.values[value]; + case 134: final int? value = readValue(buffer) as int?; return value == null ? null : PlatformBillingClientFeature.values[value]; - case 134: + case 135: final int? value = readValue(buffer) as int?; return value == null ? null : PlatformPurchaseState.values[value]; - case 135: + case 136: final int? value = readValue(buffer) as int?; return value == null ? null : PlatformRecurrenceMode.values[value]; - case 136: - return PlatformQueryProduct.decode(readValue(buffer)!); case 137: - return PlatformAccountIdentifiers.decode(readValue(buffer)!); + return PlatformQueryProduct.decode(readValue(buffer)!); case 138: - return PlatformBillingResult.decode(readValue(buffer)!); + return PlatformAccountIdentifiers.decode(readValue(buffer)!); case 139: - return PlatformOneTimePurchaseOfferDetails.decode(readValue(buffer)!); + return PlatformBillingResult.decode(readValue(buffer)!); case 140: - return PlatformProductDetails.decode(readValue(buffer)!); + return PlatformOneTimePurchaseOfferDetails.decode(readValue(buffer)!); case 141: - return PlatformProductDetailsResponse.decode(readValue(buffer)!); + return PlatformProductDetails.decode(readValue(buffer)!); case 142: + return PlatformProductDetailsResponse.decode(readValue(buffer)!); + case 143: return PlatformAlternativeBillingOnlyReportingDetailsResponse.decode( readValue(buffer)!, ); - case 143: - return PlatformBillingConfigResponse.decode(readValue(buffer)!); case 144: - return PlatformBillingFlowParams.decode(readValue(buffer)!); + return PlatformInAppMessageResult.decode(readValue(buffer)!); case 145: - return PlatformPricingPhase.decode(readValue(buffer)!); + return PlatformBillingConfigResponse.decode(readValue(buffer)!); case 146: - return PlatformPurchase.decode(readValue(buffer)!); + return PlatformBillingFlowParams.decode(readValue(buffer)!); case 147: - return PlatformPendingPurchaseUpdate.decode(readValue(buffer)!); + return PlatformPricingPhase.decode(readValue(buffer)!); case 148: - return PlatformPurchaseHistoryRecord.decode(readValue(buffer)!); + return PlatformPurchase.decode(readValue(buffer)!); case 149: - return PlatformPurchaseHistoryResponse.decode(readValue(buffer)!); + return PlatformPendingPurchaseUpdate.decode(readValue(buffer)!); case 150: - return PlatformPurchasesResponse.decode(readValue(buffer)!); + return PlatformPurchaseHistoryRecord.decode(readValue(buffer)!); case 151: - return PlatformSubscriptionOfferDetails.decode(readValue(buffer)!); + return PlatformPurchaseHistoryResponse.decode(readValue(buffer)!); case 152: - return PlatformUserChoiceDetails.decode(readValue(buffer)!); + return PlatformPurchasesResponse.decode(readValue(buffer)!); case 153: - return PlatformUserChoiceProduct.decode(readValue(buffer)!); + return PlatformSubscriptionOfferDetails.decode(readValue(buffer)!); case 154: - return PlatformInstallmentPlanDetails.decode(readValue(buffer)!); + return PlatformUserChoiceDetails.decode(readValue(buffer)!); case 155: + return PlatformUserChoiceProduct.decode(readValue(buffer)!); + case 156: + return PlatformInstallmentPlanDetails.decode(readValue(buffer)!); + case 157: return PlatformPendingPurchasesParams.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1876,6 +1950,37 @@ class InAppPurchaseApi { as PlatformAlternativeBillingOnlyReportingDetailsResponse?)!; } } + + /// Wraps BillingClient#showInAppMessages(). + Future showInAppMessages() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.showInAppMessages$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as PlatformInAppMessageResult?)!; + } + } } abstract class InAppPurchaseCallbackApi { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index c3586df9c6a7..dcf22a3acab7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -6,6 +6,7 @@ import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_inte import '../billing_client_wrappers.dart'; import 'billing_client_wrappers/billing_config_wrapper.dart'; +import 'billing_client_wrappers/in_app_message_wrapper.dart'; import 'billing_client_wrappers/pending_purchases_params_wrapper.dart'; import 'messages.g.dart'; @@ -130,6 +131,28 @@ alternativeBillingOnlyReportingDetailsWrapperFromPlatform( ); } +/// Converts [PlatformInAppMessageResponse] to its public API enum equivalent. +InAppMessageResponse inAppMessageResponseFromPlatform( + PlatformInAppMessageResponse responseCode, +) { + return switch (responseCode) { + PlatformInAppMessageResponse.noActionNeeded => + InAppMessageResponse.noActionNeeded, + PlatformInAppMessageResponse.subscriptionStatusUpdated => + InAppMessageResponse.subscriptionStatusUpdated, + }; +} + +/// Creates a [InAppMessageResultWrapper] from the Pigeon equivalent. +InAppMessageResultWrapper inAppMessageResultWrapperFromPlatform( + PlatformInAppMessageResult result, +) { + return InAppMessageResultWrapper( + responseCode: inAppMessageResponseFromPlatform(result.responseCode), + purchaseToken: result.purchaseToken, + ); +} + /// Creates a [BillingConfigWrapper] from the Pigeon equivalent. BillingConfigWrapper billingConfigWrapperFromPlatform( PlatformBillingConfigResponse response, diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 90f987cc8dfc..8047f94b581c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -118,6 +118,37 @@ class PlatformAlternativeBillingOnlyReportingDetailsResponse { final String externalTransactionToken; } +/// Response code for the in-app messaging API call. +enum PlatformInAppMessageResponse { + /// 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. +class PlatformInAppMessageResult { + PlatformInAppMessageResult({ + required this.responseCode, + required this.purchaseToken, + }); + + /// Returns response code for the in-app messaging API call. + final PlatformInAppMessageResponse responseCode; + + /// Returns token that identifies the purchase to be acknowledged, if any. + final String? purchaseToken; +} + /// Pigeon version of BillingConfigWrapper, which contains the components of the /// Java BillingConfigResponseListener callback. class PlatformBillingConfigResponse { @@ -443,6 +474,10 @@ abstract class InAppPurchaseApi { @async PlatformAlternativeBillingOnlyReportingDetailsResponse createAlternativeBillingOnlyReportingDetailsAsync(); + + /// Wraps BillingClient#showInAppMessages(). + @async + PlatformInAppMessageResult showInAppMessages(); } @FlutterApi() diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 2959ea704712..bf2ed65830c7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -684,6 +684,24 @@ void main() { expect(result, expected); }); }); + + group('showInAppMessages', () { + test('returns object', () async { + const expected = InAppMessageResultWrapper( + responseCode: InAppMessageResponse.subscriptionStatusUpdated, + purchaseToken: 'dummy purchase token', + ); + when(mockApi.showInAppMessages()).thenAnswer( + (_) async => PlatformInAppMessageResult( + responseCode: PlatformInAppMessageResponse.subscriptionStatusUpdated, + purchaseToken: expected.purchaseToken, + ), + ); + final InAppMessageResultWrapper result = await billingClient + .showInAppMessages(); + expect(result, expected); + }); + }); } PlatformBillingConfigResponse platformBillingConfigFromWrapper( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index e8d4c2c38e67..30c4526d7742 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart. // Do not manually edit this file. @@ -67,6 +67,12 @@ class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5 ) : super(parent, parentInvocation); } +class _FakePlatformInAppMessageResult_6 extends _i1.SmartFake + implements _i2.PlatformInAppMessageResult { + _FakePlatformInAppMessageResult_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [InAppPurchaseApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -390,4 +396,24 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { as _i4.Future< _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse >); + + @override + _i4.Future<_i2.PlatformInAppMessageResult> showInAppMessages() => + (super.noSuchMethod( + Invocation.method(#showInAppMessages, []), + returnValue: _i4.Future<_i2.PlatformInAppMessageResult>.value( + _FakePlatformInAppMessageResult_6( + this, + Invocation.method(#showInAppMessages, []), + ), + ), + returnValueForMissingStub: + _i4.Future<_i2.PlatformInAppMessageResult>.value( + _FakePlatformInAppMessageResult_6( + this, + Invocation.method(#showInAppMessages, []), + ), + ), + ) + as _i4.Future<_i2.PlatformInAppMessageResult>); }