diff --git a/pay/lib/src/pay.dart b/pay/lib/src/pay.dart index 97cc75fd..65c76da5 100644 --- a/pay/lib/src/pay.dart +++ b/pay/lib/src/pay.dart @@ -47,10 +47,14 @@ class Pay { /// This method wraps the [userCanPay] method in the platform interface. It /// makes sure that the [provider] exists and is available in the platform /// running the logic. - Future userCanPay(PayProvider provider) async { + /// + /// [existingPaymentMethodAvailable]: + /// - If true (default), only returns true if a supported payment method/card is available. + /// - If false, returns true if the device/user supports the payment method, regardless of card/payment method status. + Future userCanPay(PayProvider provider, {bool existingPaymentMethodAvailable = true}) async { await throwIfProviderIsNotDefined(provider); if (supportedProviders[defaultTargetPlatform]!.contains(provider)) { - return _payPlatform.userCanPay(_configurations[provider]!); + return _payPlatform.userCanPay(_configurations[provider]!, existingPaymentMethodAvailable: existingPaymentMethodAvailable); } return Future.value(false); diff --git a/pay/lib/src/widgets/pay_button.dart b/pay/lib/src/widgets/pay_button.dart index 9b08fc13..aa358a0c 100644 --- a/pay/lib/src/widgets/pay_button.dart +++ b/pay/lib/src/widgets/pay_button.dart @@ -51,6 +51,10 @@ abstract class PayButton extends StatefulWidget { /// a user can pay with it and the button loads. final Widget? loadingIndicator; + /// Determines if this button should only when there are payment methods available + /// If false, the button will show if the device supports the payment method, regardless of cards. + final bool existingPaymentMethodAvailable; + /// Initializes the button and the payment client that handles the requests. PayButton({ super.key, @@ -63,6 +67,7 @@ abstract class PayButton extends StatefulWidget { this.onError, this.childOnError, this.loadingIndicator, + this.existingPaymentMethodAvailable = false, }) : _payClient = Pay({buttonProvider: paymentConfiguration}); /// Determines the list of supported platforms for the button. @@ -136,7 +141,10 @@ class _PayButtonState extends State { Future _userCanPay() async { try { - return await widget._payClient.userCanPay(widget.buttonProvider); + return await widget._payClient.userCanPay( + widget.buttonProvider, + existingPaymentMethodAvailable: widget.existingPaymentMethodAvailable, + ); } catch (error) { widget.onError?.call(error); rethrow; 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 ca4da9e1..af872b7f 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 @@ -122,14 +122,15 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity * * This call checks whether Google Pay is supported for the pair of user and device starting a * payment operation. This call does not check whether the user has cards that conform to the - * list of supported networks unless `existingPaymentMethodRequired` is included in the + * list of supported networks unless `existingPaymentMethodAvailable` is included in the * configuration. See the docs for the [`isReadyToPay][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 callback to communicate back with the Dart end in Flutter. * @param paymentProfileString the payment configuration object in [String] format. + * @param existingPaymentMethodAvailable whether the user must have an existing payment method. */ - fun isReadyToPay(result: Result, paymentProfileString: String) { + fun isReadyToPay(result: Result, paymentProfileString: String, existingPaymentMethodAvailable: Boolean = false) { // Construct profile and client val paymentProfile = buildPaymentProfile( @@ -138,9 +139,11 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity "apiVersion", "apiVersionMinor", "allowedPaymentMethods", - "existingPaymentMethodRequired" + "existingPaymentMethodAvailable" ) ) + // Inject existingPaymentMethodAvailable into the profile + paymentProfile.put("existingPaymentMethodAvailable", existingPaymentMethodAvailable) val client = paymentClientForProfile(paymentProfile) val rtpRequest = IsReadyToPayRequest.fromJson(paymentProfile.toString()) 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 d08d4518..e48a72ed 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 @@ -75,7 +75,12 @@ class PayMethodCallHandler private constructor( @Suppress("UNCHECKED_CAST") override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { - METHOD_USER_CAN_PAY -> googlePayHandler.isReadyToPay(result, call.arguments()!!) + METHOD_USER_CAN_PAY -> { + val args = call.arguments as? Map + val paymentProfileString = args!!["paymentConfiguration"] as String + val existingPaymentMethodAvailable = args!!["existingPaymentMethodAvailable"] as? Boolean ?: false + googlePayHandler.isReadyToPay(result, paymentProfileString, existingPaymentMethodAvailable) + } METHOD_SHOW_PAYMENT_SELECTOR -> { if (eventChannelIsActive) { val arguments = call.arguments>()!! diff --git a/pay_ios/ios/Classes/PayPlugin.swift b/pay_ios/ios/Classes/PayPlugin.swift index 31ae750e..d2ad073e 100644 --- a/pay_ios/ios/Classes/PayPlugin.swift +++ b/pay_ios/ios/Classes/PayPlugin.swift @@ -21,34 +21,38 @@ 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" - + private let methodUserCanPay = "userCanPay" private let methodShowPaymentSelector = "showPaymentSelector" - + private let paymentHandler = PaymentHandler() - + public static func register(with registrar: FlutterPluginRegistrar) { let messenger = registrar.messenger() let channel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: messenger) registrar.addMethodCallDelegate(PayPlugin(), channel: channel) - + // Register the PlatformView to show the Apple Pay button. let buttonFactory = ApplePayButtonViewFactory(messenger: messenger) registrar.register(buttonFactory, withId: ApplePayButtonView.buttonMethodChannelName) } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case methodUserCanPay: - result(paymentHandler.canMakePayments(call.arguments as! String)) - + let args = call.arguments as! [String: Any] + + result(paymentHandler.canMakePayments( + args["paymentConfiguration"] as! String, + existingPaymentMethodAvailable: args["existingPaymentMethodAvailable"] as? Bool ?? false) + ) case methodShowPaymentSelector: let arguments = call.arguments as! [String: Any] paymentHandler.startPayment( result: result, paymentConfiguration: arguments["payment_profile"] as! String, paymentItems: arguments["payment_items"] as! [[String: Any?]]) - + default: result(FlutterMethodNotImplemented) } diff --git a/pay_ios/ios/Classes/PaymentHandler.swift b/pay_ios/ios/Classes/PaymentHandler.swift index 887aadf0..7bbc5425 100644 --- a/pay_ios/ios/Classes/PaymentHandler.swift +++ b/pay_ios/ios/Classes/PaymentHandler.swift @@ -36,26 +36,31 @@ enum PaymentHandlerStatus { /// paymentHandler.canMakePayments(stringArguments) /// ``` class PaymentHandler: NSObject { - + /// Holds the current status of the payment process. var paymentHandlerStatus: PaymentHandlerStatus! - + /// Stores a reference to the Flutter result while the operation completes. var paymentResult: FlutterResult! - + /// Determines whether a user can make a payment with the selected provider. /// /// - parameter paymentConfiguration: A JSON string with the configuration to execute /// this payment. - /// - returns: A boolean with the result: whether the use can make payments. - func canMakePayments(_ paymentConfiguration: String) -> Bool { + /// - parameter existingPaymentMethodAvailable: If true, requires a card; if false, only checks device support. + /// - returns: A boolean with the result: whether the user can make payments. + func canMakePayments(_ paymentConfiguration: String, existingPaymentMethodAvailable: Bool = true) -> Bool { if let supportedNetworks = PaymentHandler.supportedNetworks(from: paymentConfiguration) { - return PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks) + if existingPaymentMethodAvailable { + return PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks) + } else { + return PKPaymentAuthorizationController.canMakePayments() + } } else { return false } } - + /// Initiates the payment process with the selected payment provider. /// /// Calling this method starts the payment process and opens up the payment selector. Once the user @@ -73,13 +78,13 @@ class PaymentHandler: NSObject { // Reset payment handler status paymentHandlerStatus = .started - + // Deserialize payment configuration. guard let paymentRequest = PaymentHandler.createPaymentRequest(from: paymentConfiguration, paymentItems: paymentItems) else { result(FlutterError(code: "invalidPaymentConfiguration", message: "It was not possible to create a payment request from the provided configuration. Review your payment configuration and run again", details: nil)) return } - + // Display the payment selector with the request created. let paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) paymentController.delegate = self @@ -91,7 +96,7 @@ class PaymentHandler: NSObject { } }) } - + /// Utility function to turn the payment configuration received through the method channel into a `Dictionary`. /// /// - parameter paymentConfigurationString: A JSON string with the configuration to execute @@ -101,7 +106,7 @@ class PaymentHandler: NSObject { let paymentConfigurationData = paymentConfigurationString.data(using: .utf8) return try? JSONSerialization.jsonObject(with: paymentConfigurationData!) as? [String: Any] } - + /// Extracts and parses the list of supported networks in the payment configuration. /// /// - parameter paymentConfigurationString: A JSON string with the configuration to execute @@ -111,10 +116,10 @@ class PaymentHandler: NSObject { guard let paymentConfiguration = extractPaymentConfiguration(from: paymentConfigurationString) else { return nil } - + return (paymentConfiguration["supportedNetworks"] as! [String]).compactMap { networkString in PKPaymentNetwork.fromString(networkString) } } - + /// Creates a valid payment request for Apple Pay with the information included in the payment configuration. /// /// - parameter paymentConfigurationString: A JSON string with the configuration to execute @@ -125,7 +130,7 @@ class PaymentHandler: NSObject { guard let paymentConfiguration = extractPaymentConfiguration(from: paymentConfigurationString) else { return nil } - + // Create payment request and include summary items let paymentRequest = PKPaymentRequest() paymentRequest.paymentSummaryItems = paymentItems.map { item in @@ -135,38 +140,38 @@ class PaymentHandler: NSObject { type: (PKPaymentSummaryItemType.fromString(item["status"] as? String ?? "final_price")) ) } - + // Configure the payment. paymentRequest.merchantIdentifier = paymentConfiguration["merchantIdentifier"] as! String paymentRequest.countryCode = paymentConfiguration["countryCode"] as! String paymentRequest.currencyCode = paymentConfiguration["currencyCode"] as! String - + // Add merchant capabilities. if let merchantCapabilities = paymentConfiguration["merchantCapabilities"] as? Array { paymentRequest.merchantCapabilities = PKMerchantCapability(merchantCapabilities.compactMap { capabilityString in PKMerchantCapability.fromString(capabilityString) }) } - + // Include the shipping fields required. if let requiredShippingFields = paymentConfiguration["requiredShippingContactFields"] as? Array { paymentRequest.requiredShippingContactFields = Set(requiredShippingFields.compactMap { shippingField in PKContactField.fromString(shippingField) }) } - + // Include the billing fields required. if let requiredBillingFields = paymentConfiguration["requiredBillingContactFields"] as? Array { paymentRequest.requiredBillingContactFields = Set(requiredBillingFields.compactMap { billingField in PKContactField.fromString(billingField) }) } - + // Add supported networks if available. if let supportedNetworks = supportedNetworks(from: paymentConfigurationString) { paymentRequest.supportedNetworks = supportedNetworks } - + return paymentRequest } } @@ -177,22 +182,22 @@ extension PaymentHandler: PKPaymentAuthorizationControllerDelegate { func paymentAuthorizationControllerWillAuthorizePayment(_ controller: PKPaymentAuthorizationController) { paymentHandlerStatus = .authorizationStarted } - + func paymentAuthorizationController(_: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { - + // Collect payment result or error and return if no payment was selected guard let paymentResultData = try? JSONSerialization.data(withJSONObject: payment.toDictionary()) else { self.paymentResult(FlutterError(code: "paymentResultDeserializationFailed", message: nil, details: nil)) return } - + // Return the result back to the channel self.paymentResult(String(decoding: paymentResultData, as: UTF8.self)) - + paymentHandlerStatus = .authorized completion(PKPaymentAuthorizationResult(status: PKPaymentAuthorizationStatus.success, errors: nil)) } - + func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { controller.dismiss { DispatchQueue.main.async { diff --git a/pay_platform_interface/lib/pay_channel.dart b/pay_platform_interface/lib/pay_channel.dart index 10634ef4..47e969b5 100644 --- a/pay_platform_interface/lib/pay_channel.dart +++ b/pay_platform_interface/lib/pay_channel.dart @@ -41,10 +41,13 @@ class PayMethodChannel extends PayPlatform { /// Completes with a [PlatformException] if the native call fails or otherwise /// returns a boolean for the [paymentConfiguration] specified. @override - Future userCanPay(PaymentConfiguration paymentConfiguration) async { + Future userCanPay(PaymentConfiguration paymentConfiguration, {bool existingPaymentMethodAvailable = false}) async { return await _channel.invokeMethod( - 'userCanPay', jsonEncode(await paymentConfiguration.parameterMap())) - as bool; + 'userCanPay', { + 'paymentConfiguration': jsonEncode(await paymentConfiguration.parameterMap()), + 'existingPaymentMethodAvailable': existingPaymentMethodAvailable, + }, + ) as bool; } /// Shows the payment selector to complete the payment operation. diff --git a/pay_platform_interface/lib/pay_platform_interface.dart b/pay_platform_interface/lib/pay_platform_interface.dart index 4e65291f..007616c3 100644 --- a/pay_platform_interface/lib/pay_platform_interface.dart +++ b/pay_platform_interface/lib/pay_platform_interface.dart @@ -21,9 +21,10 @@ abstract class PayPlatform { /// Determines whether the caller can make a payment with a given /// configuration. /// - /// Returns a [Future] that resolves to a boolean value with the result based - /// on a given [paymentConfiguration]. - Future userCanPay(PaymentConfiguration paymentConfiguration); + /// [existingPaymentMethodAvailable]: + /// - If true (default), only returns true if a supported payment method/card is added. + /// - If false, returns true if the device/user supports the payment method, regardless of card/payment method status. + Future userCanPay(PaymentConfiguration paymentConfiguration, {bool existingPaymentMethodAvailable = false}); /// Triggers the action to show the payment selector to complete a payment /// with the configuration and a list of [PaymentItem] that help determine