Skip to content

Commit

Permalink
Introduce logic to distinguish between children of PayButton that use…
Browse files Browse the repository at this point in the history
… an event stream to report payment status updates back to the widgets and those that operate synchronously

Update Google Pay button to use an event stream and avoid lifecycle issues
  • Loading branch information
JlUgia committed Oct 10, 2024
1 parent 91cc7dd commit 1b0887c
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 17 deletions.
2 changes: 2 additions & 0 deletions pay/lib/pay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions pay/lib/src/widgets/apple_pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ class ApplePayButton extends PayButton {

@override
late final Widget _payButton = _applePayButton;

@override
final bool _returnsPaymentDataSynchronously = true;
}
42 changes: 42 additions & 0 deletions pay/lib/src/widgets/google_pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,46 @@ class GooglePayButton extends PayButton {

@override
late final Widget _payButton = _googlePayButton;

@override
final bool _returnsPaymentDataSynchronously = 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()
.map((result) => jsonDecode(result as String) as Map<String, dynamic>)
.listen((result) {
widget._deliverPaymentResult(result);
}, onError: (error) {
widget._deliverError(error);
});
}

@override
void initState() {
super.initState();
_startListeningForPaymentResults();
}

@override
void dispose() {
_paymentResultSubscription?.cancel();
_paymentResultSubscription = null;
super.dispose();
}
}
17 changes: 15 additions & 2 deletions pay/lib/src/widgets/pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ abstract class PayButton extends StatefulWidget {
this.loadingIndicator,
}) : _payClient = Pay({buttonProvider: paymentConfiguration});


/// 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 _returnsPaymentDataSynchronously;
/// Callback function to respond to tap events.
///
/// This is the default function for tap events. Calls the [onPressed]
Expand All @@ -77,9 +84,9 @@ abstract class PayButton extends StatefulWidget {
try {
final result =
await _payClient.showPaymentSelector(buttonProvider, paymentItems);
onPaymentResult?.call(result);
if (_returnsPaymentDataSynchronously) _deliverPaymentResult(result);
} catch (error) {
onError?.call(error);
_deliverError(error);
}
};
}
Expand All @@ -96,9 +103,15 @@ abstract class PayButton extends StatefulWidget {
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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
Expand All @@ -47,7 +48,8 @@ private const val LOAD_PAYMENT_DATA_REQUEST_CODE = 991
*/
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 {

Expand Down Expand Up @@ -101,6 +103,12 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity
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
}

/**
Expand Down Expand Up @@ -150,10 +158,6 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity
fun loadPaymentData(
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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"
Expand All @@ -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(
Expand All @@ -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
}
}

0 comments on commit 1b0887c

Please sign in to comment.