Repository: google-pay/flutter-plugin

Issue:
Crash when calling DataResult multiple times on the same instance
We are facing a problem with the library related to Google Pay, the app crashes if the DataResult object (callback) is called more than one time per instance.
![Screenshot 2024-09-04 at 10 27 26 AM](https://github.com/user-attachments/assets/581f2c9a-ec7b-44c2-9efd-1b5fa4824098)
I was able reproduce it directly on the Pay library
![20240903_190431_113635](https://github.com/user-attachments/assets/5fef0657-28e1-4cea-89af-0bba2bb7fbd3)




Fix patch:
diff --git a/pay/README.md b/pay/README.md
index 68f0612c..b9795024 100644
--- a/pay/README.md
+++ b/pay/README.md
@@ -30,7 +30,7 @@ dependencies:
 Define the configuration for your payment provider(s). Take a look at the parameters available in the documentation for [Apple Pay](https://developer.apple.com/documentation/passkit/pkpaymentrequest) and [Google Pay](https://developers.google.com/pay/api/android/reference/request-objects), and explore the [sample configurations in this package](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart).
 
 ### Example
-This snippet assumes the existence a payment configuration for Apple Pay ([`defaultApplePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L27)) and another one for Google Pay ([`defaultGooglePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L63)):
+This snippet assumes the existence of a payment configuration for Apple Pay ([`defaultApplePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L27)) and another one for Google Pay ([`defaultGooglePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L63)):
 ```dart
 import 'package:pay/pay.dart';
 
@@ -117,15 +117,10 @@ const _paymentItems = [
   )
 ];
 
-late final Pay _payClient;
-
-// When you are ready to load your configuration
-_payClient = Pay({
-    PayProvider.google_pay: PaymentConfiguration.fromJsonString(
-        payment_configurations.defaultGooglePay),
-    PayProvider.apple_pay: PaymentConfiguration.fromJsonString(
-        payment_configurations.defaultApplePay),
-  });
+final Pay _payClient = Pay({
+  PayProvider.google_pay: payment_configurations.defaultGooglePayConfig,
+  PayProvider.apple_pay: payment_configurations.defaultApplePayConfig,
+});
 ```
 
 As you can see, you can add multiple configurations to your payment client, one for each payment provider supported.
@@ -141,8 +136,10 @@ Widget build(BuildContext context) {
       if (snapshot.connectionState == ConnectionState.done) {
         if (snapshot.data == true) {
           return RawGooglePayButton(
+              paymentConfiguration:
+                  payment_configurations.defaultGooglePayConfig,
               type: GooglePayButtonType.buy,
-              onPressed: onGooglePayPressed);
+              onPressed: _onGooglePayPressed);
         } else {
           // userCanPay returned false
           // Consider showing an alternative payment method
@@ -158,7 +155,7 @@ Widget build(BuildContext context) {
 Finally, handle the `onPressed` event and trigger the payment selector as follows:
 
 ```dart
-void onGooglePayPressed() async {
+void _onGooglePayPressed() async {
   final result = await _payClient.showPaymentSelector(
     PayProvider.google_pay,
     _paymentItems,
@@ -167,6 +164,33 @@ void onGooglePayPressed() async {
 }
 ```
 
+### Handling a payment result response (Android only)
+On Android, payment results are received using an event channel, in order to eliminate the effect of lost references during activity recreation events. Because of that, calls to `Pay.showPaymentSelector` only initiate the payment process and don't return any result.
+
+To subscribe to the result stream, create an `EventChannel` using the payment results channel name (`plugins.flutter.io/pay/payment_result`) and start listening to events:
+
+```dart
+static const eventChannel =
+    EventChannel('plugins.flutter.io/pay/payment_result');
+final _paymentResultSubscription = eventChannel
+    .receiveBroadcastStream()
+    .map((result) => jsonDecode(result as String) as Map<String, dynamic>)
+    .listen((result) {
+      // TODO: Send the resulting Google Pay token to your server / PSP
+    }, onError: (error) {
+      // TODO: Handle errors
+    });
+```
+
+Make sure to cancel the subscription and clear the reference when it is not needed anymore:
+
+```dart
+_paymentResultSubscription.cancel();
+_paymentResultSubscription = null;
+```
+
+See the [advanced example](https://github.com/google-pay/flutter-plugin/blob/main/pay/example/lib/advanced.dart) to see a working integration.
+
 ## Additional resources
 Take a look at the following resources to manage your payment accounts and learn more about the APIs for the supported providers:
 
diff --git a/pay/example/ios/Runner/Info.plist b/pay/example/ios/Runner/Info.plist
index 0c4df0b5..b77ef2a9 100644
--- a/pay/example/ios/Runner/Info.plist
+++ b/pay/example/ios/Runner/Info.plist
@@ -43,5 +43,7 @@
 	<false/>
 	<key>CADisableMinimumFrameDurationOnPhone</key>
 	<true/>
+	<key>UIApplicationSupportsIndirectInputEvents</key>
+	<true/>
 </dict>
 </plist>
diff --git a/pay/example/lib/advanced.dart b/pay/example/lib/advanced.dart
new file mode 100644
index 00000000..c67d5774
--- /dev/null
+++ b/pay/example/lib/advanced.dart
@@ -0,0 +1,223 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:pay/pay.dart';
+
+import 'payment_configurations.dart' as payment_configurations;
+
+void main() {
+  runApp(const PayAdvancedMaterialApp());
+}
+
+const googlePayEventChannelName = 'plugins.flutter.io/pay/payment_result';
+const _paymentItems = [
+  PaymentItem(
+    label: 'Total',
+    amount: '99.99',
+    status: PaymentItemStatus.final_price,
+  )
+];
+
+class PayAdvancedMaterialApp extends StatelessWidget {
+  const PayAdvancedMaterialApp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Pay for Flutter Advanced Integration Demo',
+      localizationsDelegates: const [
+        ...GlobalMaterialLocalizations.delegates,
+        GlobalWidgetsLocalizations.delegate,
+      ],
+      supportedLocales: const [
+        Locale('en', ''),
+        Locale('es', ''),
+        Locale('de', ''),
+      ],
+      home: PayAdvancedSampleApp(),
+    );
+  }
+}
+
+class PayAdvancedSampleApp extends StatefulWidget {
+  final Pay payClient;
+
+  PayAdvancedSampleApp({super.key})
+      : payClient = Pay({
+          PayProvider.google_pay: payment_configurations.defaultGooglePayConfig,
+          PayProvider.apple_pay: payment_configurations.defaultApplePayConfig,
+        });
+
+  @override
+  State<PayAdvancedSampleApp> createState() => _PayAdvancedSampleAppState();
+}
+
+class _PayAdvancedSampleAppState extends State<PayAdvancedSampleApp> {
+  static const eventChannel = EventChannel(googlePayEventChannelName);
+  StreamSubscription<String>? _googlePayResultSubscription;
+
+  late final Future<bool> _canPayGoogleFuture;
+  late final Future<bool> _canPayAppleFuture;
+
+  // A method to listen to events coming from the event channel on Android
+  void _startListeningForPaymentResults() {
+    _googlePayResultSubscription = eventChannel
+        .receiveBroadcastStream()
+        .map((result) => result.toString())
+        .listen(debugPrint, onError: (error) => debugPrint(error.toString()));
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      _startListeningForPaymentResults();
+    }
+
+    // Initialize userCanPay futures
+    _canPayGoogleFuture = widget.payClient.userCanPay(PayProvider.google_pay);
+    _canPayAppleFuture = widget.payClient.userCanPay(PayProvider.apple_pay);
+  }
+
+  void _onGooglePayPressed() =>
+      _showPaymentSelectorForProvider(PayProvider.google_pay);
+
+  void _onApplePayPressed() =>
+      _showPaymentSelectorForProvider(PayProvider.apple_pay);
+
+  void _showPaymentSelectorForProvider(PayProvider provider) async {
+    try {
+      final result =
+          await widget.payClient.showPaymentSelector(provider, _paymentItems);
+      debugPrint(result.toString());
+    } catch (error) {
+      debugPrint(error.toString());
+    }
+  }
+
+  @override
+  void dispose() {
+    _googlePayResultSubscription?.cancel();
+    _googlePayResultSubscription = null;
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('T-shirt Shop'),
+      ),
+      backgroundColor: Colors.white,
+      body: ListView(
+        padding: const EdgeInsets.symmetric(horizontal: 20),
+        children: [
+          Container(
+            margin: const EdgeInsets.symmetric(vertical: 20),
+            child: const Image(
+              image: AssetImage('assets/images/ts_10_11019a.jpg'),
+              height: 350,
+            ),
+          ),
+          const Text(
+            'Amanda\'s Polo Shirt',
+            style: TextStyle(
+              fontSize: 20,
+              color: Color(0xff333333),
+              fontWeight: FontWeight.bold,
+            ),
+          ),
+          const SizedBox(height: 5),
+          const Text(
+            '\$50.20',
+            style: TextStyle(
+              color: Color(0xff777777),
+              fontSize: 15,
+            ),
+          ),
+          const SizedBox(height: 15),
+          const Text(
+            'Description',
+            style: TextStyle(
+              fontSize: 15,
+              color: Color(0xff333333),
+              fontWeight: FontWeight.bold,
+            ),
+          ),
+          const SizedBox(height: 5),
+          const Text(
+            'A versatile full-zip that you can wear all day long and even...',
+            style: TextStyle(
+              color: Color(0xff777777),
+              fontSize: 15,
+            ),
+          ),
+          const SizedBox(height: 15),
+
+          // Google Pay button
+          FutureBuilder<bool>(
+            future: _canPayGoogleFuture,
+            builder: (context, snapshot) {
+              if (snapshot.connectionState == ConnectionState.done) {
+                if (snapshot.data == true) {
+                  return RawGooglePayButton(
+                      paymentConfiguration:
+                          payment_configurations.defaultGooglePayConfig,
+                      type: GooglePayButtonType.buy,
+                      onPressed: _onGooglePayPressed);
+                } else {
+                  // userCanPay returned false
+                  // Consider showing an alternative payment method
+                }
+              } else {
+                // The operation hasn't finished loading
+                // Consider showing a loading indicator
+              }
+              // This example shows an empty box if userCanPay returns false
+              return const SizedBox.shrink();
+            },
+          ),
+
+          // Apple Pay button
+          FutureBuilder<bool>(
+            future: _canPayAppleFuture,
+            builder: (context, snapshot) {
+              if (snapshot.connectionState == ConnectionState.done) {
+                if (snapshot.data == true) {
+                  return RawApplePayButton(
+                      type: ApplePayButtonType.buy,
+                      onPressed: _onApplePayPressed);
+                } else {
+                  // userCanPay returned false
+                  // Consider showing an alternative payment method
+                }
+              } else {
+                // The operation hasn't finished loading
+                // Consider showing a loading indicator
+              }
+              // This example shows an empty box if userCanPay returns false
+              return const SizedBox.shrink();
+            },
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/pay/example/lib/payment_configurations.dart b/pay/example/lib/payment_configurations.dart
index 7e8eef26..60b693b1 100644
--- a/pay/example/lib/payment_configurations.dart
+++ b/pay/example/lib/payment_configurations.dart
@@ -23,6 +23,12 @@
 /// application.
 library;
 
+import 'package:pay/pay.dart';
+
+/// Sample [PaymentConfiguration] for Apple Pay
+final defaultApplePayConfig =
+    PaymentConfiguration.fromJsonString(defaultApplePay);
+
 /// Sample configuration for Apple Pay. Contains the same content as the file
 /// under `assets/default_payment_profile_apple_pay.json`.
 const String defaultApplePay = '''{
@@ -59,6 +65,10 @@ const String defaultApplePay = '''{
   }
 }''';
 
+/// Sample [PaymentConfiguration] for Google Pay
+final defaultGooglePayConfig =
+    PaymentConfiguration.fromJsonString(defaultGooglePay);
+
 /// Sample configuration for Google Pay. Contains the same content as the file
 /// under `assets/default_payment_profile_google_pay.json`.
 const String defaultGooglePay = '''{
diff --git a/pay/lib/pay.dart b/pay/lib/pay.dart
index 264e49ef..18b9e591 100644
--- a/pay/lib/pay.dart
+++ b/pay/lib/pay.dart
@@ -14,10 +14,12 @@
 
 library pay;
 
+import 'dart:convert';
 import 'dart:async';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:pay_ios/pay_ios.dart';
 import 'package:pay_android/pay_android.dart';
 import 'package:pay_platform_interface/core/payment_configuration.dart';
diff --git a/pay/lib/src/widgets/apple_pay_button.dart b/pay/lib/src/widgets/apple_pay_button.dart
index 2016667d..48ebecf8 100644
--- a/pay/lib/src/widgets/apple_pay_button.dart
+++ b/pay/lib/src/widgets/apple_pay_button.dart
@@ -64,4 +64,7 @@ class ApplePayButton extends PayButton {
 
   @override
   late final Widget _payButton = _applePayButton;
+
+  @override
+  final bool _collectPaymentResultSynchronously = true;
 }
diff --git a/pay/lib/src/widgets/google_pay_button.dart b/pay/lib/src/widgets/google_pay_button.dart
index ec273662..16ac1455 100644
--- a/pay/lib/src/widgets/google_pay_button.dart
+++ b/pay/lib/src/widgets/google_pay_button.dart
@@ -66,4 +66,44 @@ class GooglePayButton extends PayButton {
 
   @override
   late final Widget _payButton = _googlePayButton;
+
+  @override
+  final bool _collectPaymentResultSynchronously = false;
+
+  @override
+  State<PayButton> createState() => _GooglePayButtonState();
+}
+
+class _GooglePayButtonState extends _PayButtonState {
+  static const eventChannel =
+      EventChannel('plugins.flutter.io/pay/payment_result');
+  StreamSubscription<Map<String, dynamic>>? _paymentResultSubscription;
+
+  /// A method to listen to events coming from the event channel on Android
+  ///
+  /// The Android implementation uses an event stream to receive payment results
+  /// in order to circumvent the loss of connection with the Flutter engine when
+  /// the main activity hosting the plugin gets recycled due to resource
+  /// exhaustion.
+  void _startListeningForPaymentResults() {
+    _paymentResultSubscription = eventChannel
+        .receiveBroadcastStream()
+        .cast<String>()
+        .map(jsonDecode)
+        .cast<Map<String, dynamic>>()
+        .listen(widget._deliverPaymentResult, onError: widget._deliverError);
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _startListeningForPaymentResults();
+  }
+
+  @override
+  void dispose() {
+    _paymentResultSubscription?.cancel();
+    _paymentResultSubscription = null;
+    super.dispose();
+  }
 }
diff --git a/pay/lib/src/widgets/pay_button.dart b/pay/lib/src/widgets/pay_button.dart
index 55337e9c..0a42ee96 100644
--- a/pay/lib/src/widgets/pay_button.dart
+++ b/pay/lib/src/widgets/pay_button.dart
@@ -27,7 +27,7 @@ part of '../../pay.dart';
 /// method which starts the payment process.
 abstract class PayButton extends StatefulWidget {
   /// A resident client to issue requests against the APIs.
-  late final Pay _payClient;
+  final Pay _payClient;
 
   /// Specifies the payment provider supported by the button
   final PayProvider buttonProvider;
@@ -65,6 +65,29 @@ abstract class PayButton extends StatefulWidget {
     this.loadingIndicator,
   }) : _payClient = Pay({buttonProvider: paymentConfiguration});
 
+  /// Determines the list of supported platforms for the button.
+  List<TargetPlatform> get _supportedPlatforms;
+
+  /// Accessor for the widget to show as the payment button.
+  ///
+  /// This method returns a [Widget] that is conditionally shown based on the
+  /// result of the `isReadyToPay` request.
+  Widget get _payButton;
+
+  /// Defines the strategy to return payment data information to the caller.
+  ///
+  /// This field is defined by implementations of this class to determine if the
+  /// payment result is returned right after calling [Pay.showPaymentSelector]
+  /// or rather received through asynchronous means (e.g.: an event stream).
+  bool get _collectPaymentResultSynchronously;
+
+  /// Determines whether the current platform is supported by the button.
+  bool get _isPlatformSupported =>
+      _supportedPlatforms.contains(defaultTargetPlatform);
+
+  @override
+  State<PayButton> createState() => _PayButtonState();
+
   /// Callback function to respond to tap events.
   ///
   /// This is the default function for tap events. Calls the [onPressed]
@@ -77,28 +100,20 @@ abstract class PayButton extends StatefulWidget {
       try {
         final result =
             await _payClient.showPaymentSelector(buttonProvider, paymentItems);
-        onPaymentResult?.call(result);
+        if (_collectPaymentResultSynchronously) _deliverPaymentResult(result);
       } catch (error) {
-        onError?.call(error);
+        _deliverError(error);
       }
     };
   }
 
-  /// Determines the list of supported platforms for the button.
-  List<TargetPlatform> get _supportedPlatforms;
-
-  /// Accessor for the widget to show as the payment button.
-  ///
-  /// This method returns a [Widget] that is conditionally shown based on the
-  /// result of the `isReadyToPay` request.
-  Widget get _payButton;
-
-  /// Determines whether the current platform is supported by the button.
-  bool get _isPlatformSupported =>
-      _supportedPlatforms.contains(defaultTargetPlatform);
+  void _deliverPaymentResult(Map<String, dynamic> result) {
+    onPaymentResult?.call(result);
+  }
 
-  @override
-  State<PayButton> createState() => _PayButtonState();
+  void _deliverError(error) {
+    onError?.call(error);
+  }
 }
 
 /// Button state that adds the widgets to the tree and holds the result of the
@@ -109,9 +124,9 @@ abstract class PayButton extends StatefulWidget {
 /// [_payButton] is added to the tree. Otherwise, if set, the replacement widget
 /// in [childOnError] is shown.
 class _PayButtonState extends State<PayButton> {
-  late final Future<bool> userCanPayFuture;
+  late final Future<bool> _userCanPayFuture;
 
-  Future<bool> userCanPay() async {
+  Future<bool> _userCanPay() async {
     try {
       return await widget._payClient.userCanPay(widget.buttonProvider);
     } catch (error) {
@@ -123,7 +138,7 @@ class _PayButtonState extends State<PayButton> {
   @override
   void initState() {
     super.initState();
-    userCanPayFuture = userCanPay();
+    _userCanPayFuture = _userCanPay();
   }
 
   @override
@@ -135,7 +150,7 @@ class _PayButtonState extends State<PayButton> {
     // Future builder running the `userCanPayFuture` and decides what to show
     // based on the result.
     return FutureBuilder<bool>(
-      future: userCanPayFuture,
+      future: _userCanPayFuture,
       builder: (context, snapshot) {
         if (snapshot.connectionState == ConnectionState.done) {
           if (snapshot.data == true) {
diff --git a/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/GooglePayHandler.kt b/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/GooglePayHandler.kt
index f1c1340a..0a517e5d 100644
--- a/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/GooglePayHandler.kt
+++ b/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/GooglePayHandler.kt
@@ -20,7 +20,13 @@ import android.app.Activity
 import android.content.Intent
 import com.google.android.gms.common.api.ApiException
 import com.google.android.gms.common.api.CommonStatusCodes
-import com.google.android.gms.wallet.*
+import com.google.android.gms.wallet.AutoResolveHelper
+import com.google.android.gms.wallet.IsReadyToPayRequest
+import com.google.android.gms.wallet.PaymentData
+import com.google.android.gms.wallet.PaymentDataRequest
+import com.google.android.gms.wallet.PaymentsClient
+import com.google.android.gms.wallet.Wallet
+import io.flutter.plugin.common.EventChannel.EventSink
 import io.flutter.plugin.common.MethodChannel.Result
 import io.flutter.plugin.common.PluginRegistry
 import io.flutter.plugins.pay_android.util.PaymentsUtil
@@ -40,10 +46,10 @@ private const val LOAD_PAYMENT_DATA_REQUEST_CODE = 991
  * ```
  * @property activity the activity used by the plugin binding.
  */
-class GooglePayHandler(private val activity: Activity) :
-        PluginRegistry.ActivityResultListener {
+class GooglePayHandler(private val activity: Activity) : PluginRegistry.ActivityResultListener {
 
-    private lateinit var loadPaymentDataResult: Result
+    // The stream sink that relays messages back to the Flutter end
+    private var paymentResultEvents: EventSink? = null
 
     companion object {
 
@@ -58,8 +64,7 @@ class GooglePayHandler(private val activity: Activity) :
          */
         @JvmStatic
         fun buildPaymentProfile(
-                paymentProfileString: String,
-                paymentItems: List<Map<String, Any?>>? = null
+            paymentProfileString: String, paymentItems: List<Map<String, Any?>>? = null
         ): JSONObject {
             val paymentProfile = JSONObject(paymentProfileString)
 
@@ -82,26 +87,28 @@ class GooglePayHandler(private val activity: Activity) :
     }
 
     /**
-     * Create a [PaymentsClient] with the configuration specified.
+     * Creates a [PaymentsClient] with the configuration specified in the [paymentProfile].
      *
      * This client is used by the Google Pay library to make calls against the API.
      *
-     * @param paymentProfile the payment configuration object.
-     * @return a [PaymentsClient] object with the configuration above.
-     *
      * @see <a href="https://developers.google.com/pay/api/android/reference/client">Client
      * reference</a>.
      * more.
      */
     private fun paymentClientForProfile(paymentProfile: JSONObject): PaymentsClient {
-        val environmentConstant = PaymentsUtil
-                .environmentForString(paymentProfile["environment"] as String?)
+        val environmentConstant =
+            PaymentsUtil.environmentForString(paymentProfile["environment"] as String?)
 
         return Wallet.getPaymentsClient(
-                activity,
-                Wallet.WalletOptions.Builder()
-                        .setEnvironment(environmentConstant)
-                        .build())
+            activity, Wallet.WalletOptions.Builder().setEnvironment(environmentConstant).build()
+        )
+    }
+
+    /**
+     * Sets or unsets a new value for the stream sink to deliver messages back to Flutter.
+     */
+    fun setPaymentResultEventSink(eventSink: EventSink?) {
+        paymentResultEvents = eventSink
     }
 
     /**
@@ -129,9 +136,10 @@ class GooglePayHandler(private val activity: Activity) :
                 result.success(completedTask.getResult(ApiException::class.java))
             } catch (exception: Exception) {
                 result.error(
-                        PaymentsUtil.statusCodeForException(exception).toString(),
-                        exception.message,
-                        null)
+                    PaymentsUtil.statusCodeForException(exception).toString(),
+                    exception.message,
+                    null
+                )
             }
         }
     }
@@ -144,32 +152,24 @@ class GooglePayHandler(private val activity: Activity) :
      * the [`loadPaymentData`][https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentsClient#isReadyToPay(com.google.android.gms.wallet.IsReadyToPayRequest)]
      * call to learn more.
      *
-     * @param result te reference to the result to send the response back to Flutter.
      * @param paymentProfileString a JSON string with the configuration to execute this payment.
      * @param paymentItems a list of payment elements that determine the total amount purchased.
      */
     fun loadPaymentData(
-            result: Result,
-            paymentProfileString: String,
-            paymentItems: List<Map<String, Any?>>
+        paymentProfileString: String, paymentItems: List<Map<String, Any?>>
     ) {
-
-        // Update the member to call the result when the operation completes
-        loadPaymentDataResult = result
-
         val paymentProfile = buildPaymentProfile(paymentProfileString, paymentItems)
         val client = paymentClientForProfile(paymentProfile)
         val ldpRequest = PaymentDataRequest.fromJson(paymentProfile.toString())
         AutoResolveHelper.resolveTask(
-                client.loadPaymentData(ldpRequest),
-                activity,
-                LOAD_PAYMENT_DATA_REQUEST_CODE)
+            client.loadPaymentData(ldpRequest), activity, LOAD_PAYMENT_DATA_REQUEST_CODE
+        )
     }
 
     override fun onActivityResult(
-            requestCode: Int,
-            resultCode: Int,
-            data: Intent?,
+        requestCode: Int,
+        resultCode: Int,
+        data: Intent?,
     ): Boolean = when (requestCode) {
         LOAD_PAYMENT_DATA_REQUEST_CODE -> {
             when (resultCode) {
@@ -183,10 +183,9 @@ class GooglePayHandler(private val activity: Activity) :
                 }
 
                 Activity.RESULT_CANCELED -> {
-                    loadPaymentDataResult.error(
-                            "paymentCanceled",
-                            "User canceled payment authorization",
-                            null)
+                    paymentResultEvents?.error(
+                        "paymentCanceled", "User canceled payment authorization", null
+                    )
                     true
                 }
 
@@ -200,25 +199,24 @@ class GooglePayHandler(private val activity: Activity) :
                 else -> false
             }
         }
+
         else -> false
     }
 
     /**
-     * Takes the result after showing the payment selector and uses it to create a response.
-     *
-     * @param paymentData a response object returned by Google Pay after a payer approves payment.
+     * Receives the [paymentData] result returned by Google Pay after showing the payment selector
+     * and uses it to create a response.
      *
      * @see [Payment
      * Data](https://developers.google.com/pay/api/android/reference/object.PaymentData)
      */
     private fun handlePaymentSuccess(paymentData: PaymentData?) {
         if (paymentData != null) {
-            loadPaymentDataResult.success(paymentData.toJson())
+            paymentResultEvents?.success(paymentData.toJson())
         } else {
-            loadPaymentDataResult.error(
-                    CommonStatusCodes.INTERNAL_ERROR.toString(),
-                    "Unexpected empty result data.",
-                    null)
+            paymentResultEvents?.error(
+                CommonStatusCodes.INTERNAL_ERROR.toString(), "Unexpected empty result data.", null
+            )
         }
     }
 
@@ -235,5 +233,5 @@ class GooglePayHandler(private val activity: Activity) :
      * Wallet constants library](https://developers.google.com/android/reference/com/google/android/gms/wallet/WalletConstants.constant-summary)
      */
     private fun handleError(statusCode: Int) =
-            loadPaymentDataResult.error(statusCode.toString(), "", null)
+        paymentResultEvents?.error(statusCode.toString(), "", null)
 }
diff --git a/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayMethodCallHandler.kt b/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayMethodCallHandler.kt
index 1ceea0c7..d08d4518 100644
--- a/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayMethodCallHandler.kt
+++ b/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayMethodCallHandler.kt
@@ -20,12 +20,16 @@ import android.app.Activity
 import io.flutter.embedding.engine.plugins.FlutterPlugin
 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
 import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.EventChannel.EventSink
+import io.flutter.plugin.common.EventChannel.StreamHandler
 import io.flutter.plugin.common.MethodCall
 import io.flutter.plugin.common.MethodChannel
 import io.flutter.plugin.common.MethodChannel.MethodCallHandler
 import io.flutter.plugin.common.MethodChannel.Result
 
-private const val METHOD_CHANNEL_NAME = "plugins.flutter.io/pay_channel"
+private const val METHOD_CHANNEL_NAME = "plugins.flutter.io/pay"
+private const val PAYMENT_EVENT_CHANNEL_NAME = "plugins.flutter.io/pay/payment_result"
 
 private const val METHOD_USER_CAN_PAY = "userCanPay"
 private const val METHOD_SHOW_PAYMENT_SELECTOR = "showPaymentSelector"
@@ -37,13 +41,18 @@ class PayMethodCallHandler private constructor(
     messenger: BinaryMessenger,
     activity: Activity,
     private val activityBinding: ActivityPluginBinding?,
-) : MethodCallHandler {
+) : MethodCallHandler, StreamHandler {
+
+    private val methodChannel: MethodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME)
+    private var eventChannelIsActive = false
+    private val paymentResultEventChannel: EventChannel =
+        EventChannel(messenger, PAYMENT_EVENT_CHANNEL_NAME)
 
-    private val channel: MethodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME)
     private val googlePayHandler: GooglePayHandler = GooglePayHandler(activity)
 
     init {
-        channel.setMethodCallHandler(this)
+        methodChannel.setMethodCallHandler(this)
+        paymentResultEventChannel.setStreamHandler(this)
     }
 
     constructor(
@@ -57,24 +66,45 @@ class PayMethodCallHandler private constructor(
      * Clears the handler in the method channel when not needed anymore.
      */
     fun stopListening() {
-        channel.setMethodCallHandler(null)
+        methodChannel.setMethodCallHandler(null)
+        paymentResultEventChannel.setStreamHandler(null)
         activityBinding?.removeActivityResultListener(googlePayHandler)
     }
 
+    // MethodCallHandler interface
     @Suppress("UNCHECKED_CAST")
     override fun onMethodCall(call: MethodCall, result: Result) {
         when (call.method) {
             METHOD_USER_CAN_PAY -> googlePayHandler.isReadyToPay(result, call.arguments()!!)
             METHOD_SHOW_PAYMENT_SELECTOR -> {
-                val arguments = call.arguments<Map<String, Any>>()!!
-                googlePayHandler.loadPaymentData(
-                    result,
-                    arguments.getValue("payment_profile") as String,
-                    arguments.getValue("payment_items") as List<Map<String, Any?>>
-                )
+                if (eventChannelIsActive) {
+                    val arguments = call.arguments<Map<String, Any>>()!!
+                    googlePayHandler.loadPaymentData(
+                        arguments.getValue("payment_profile") as String,
+                        arguments.getValue("payment_items") as List<Map<String, Any?>>
+                    )
+                    result.success("{}")
+                } else {
+                    result.error(
+                        "illegalEventChannelState",
+                        "Your event channel stream needs to be initialized and listening before calling the `showPaymentSelector` method. See the integration tutorial to learn more (https://pub.dev/packages/pay#advanced-usage)",
+                        null
+                    )
+                }
             }
 
             else -> result.notImplemented()
         }
     }
+
+    // StreamHandler interface
+    override fun onListen(arguments: Any?, events: EventSink?) {
+        googlePayHandler.setPaymentResultEventSink(events)
+        eventChannelIsActive = true
+    }
+
+    override fun onCancel(arguments: Any?) {
+        googlePayHandler.setPaymentResultEventSink(null)
+        eventChannelIsActive = false
+    }
 }
diff --git a/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayPlugin.kt b/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayPlugin.kt
index d8895206..2927840f 100644
--- a/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayPlugin.kt
+++ b/pay_android/android/src/main/kotlin/io/flutter/plugins/pay_android/PayPlugin.kt
@@ -34,7 +34,8 @@ class PayPlugin : FlutterPlugin, ActivityAware {
     override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
         this.flutterPluginBinding = flutterPluginBinding
         flutterPluginBinding.platformViewRegistry.registerViewFactory(
-            googlePayButtonViewType, PayButtonViewFactory(flutterPluginBinding.binaryMessenger))
+            googlePayButtonViewType, PayButtonViewFactory(flutterPluginBinding.binaryMessenger)
+        )
     }
 
     override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit
@@ -46,7 +47,7 @@ class PayPlugin : FlutterPlugin, ActivityAware {
     override fun onDetachedFromActivity() = methodCallHandler.stopListening()
 
     override fun onReattachedToActivityForConfigChanges(
-            activityPluginBinding: ActivityPluginBinding,
+        activityPluginBinding: ActivityPluginBinding,
     ) = onAttachedToActivity(activityPluginBinding)
 
     override fun onDetachedFromActivityForConfigChanges() = onDetachedFromActivity()
diff --git a/pay_ios/ios/Classes/PayPlugin.swift b/pay_ios/ios/Classes/PayPlugin.swift
index 51da04c1..31ae750e 100644
--- a/pay_ios/ios/Classes/PayPlugin.swift
+++ b/pay_ios/ios/Classes/PayPlugin.swift
@@ -20,7 +20,8 @@ import UIKit
 
 /// A class that receives and handles calls from Flutter to complete the payment.
 public class PayPlugin: NSObject, FlutterPlugin {
-  private static let methodChannelName = "plugins.flutter.io/pay_channel"
+  private static let methodChannelName = "plugins.flutter.io/pay"
+  
   private let methodUserCanPay = "userCanPay"
   private let methodShowPaymentSelector = "showPaymentSelector"
   
diff --git a/pay_platform_interface/lib/pay_channel.dart b/pay_platform_interface/lib/pay_channel.dart
index 0a15a217..10634ef4 100644
--- a/pay_platform_interface/lib/pay_channel.dart
+++ b/pay_platform_interface/lib/pay_channel.dart
@@ -33,9 +33,8 @@ import 'pay_platform_interface.dart';
 ///     configuration, paymentItems);
 /// ```
 class PayMethodChannel extends PayPlatform {
-  // The channel used to send messages down the native pipe.
-  final MethodChannel _channel =
-      const MethodChannel('plugins.flutter.io/pay_channel');
+  // The method channel used to send messages down the native pipe.
+  final MethodChannel _channel = const MethodChannel('plugins.flutter.io/pay');
 
   /// Determines whether a user can pay with the provider in the configuration.
   ///


Test patch:
diff --git a/pay_platform_interface/test/pay_channel_test.dart b/pay_platform_interface/test/pay_channel_test.dart
index 32002c0c..71dd0417 100644
--- a/pay_platform_interface/test/pay_channel_test.dart
+++ b/pay_platform_interface/test/pay_channel_test.dart
@@ -24,7 +24,7 @@ void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
   late final PayMethodChannel mobilePlatform;
-  const channel = MethodChannel('plugins.flutter.io/pay_channel');
+  const channel = MethodChannel('plugins.flutter.io/pay');
 
   const providerApplePay = PayProvider.apple_pay;
   final payConfigString =