diff --git a/pay/lib/src/pay.dart b/pay/lib/src/pay.dart index 97cc75fd..de006ef5 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 { + /// + /// [existingPaymentMethodRequired]: + /// - If true (default), only returns true if a supported payment method is available. + /// - If false, returns true if the device and user support the payment method, even if none is currently available. + Future userCanPay(PayProvider provider, {bool existingPaymentMethodRequired = true}) async { await throwIfProviderIsNotDefined(provider); if (supportedProviders[defaultTargetPlatform]!.contains(provider)) { - return _payPlatform.userCanPay(_configurations[provider]!); + return _payPlatform.userCanPay(_configurations[provider]!, existingPaymentMethodRequired: existingPaymentMethodRequired); } return Future.value(false); diff --git a/pay/lib/src/widgets/pay_button.dart b/pay/lib/src/widgets/pay_button.dart index c203ffaf..2de9a043 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; + /// Whether an existing payment method is required to show this button. + /// If false, the button is shown whenever the device supports the payment method. + final bool existingPaymentMethodRequired; + /// 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.existingPaymentMethodRequired = 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, + existingPaymentMethodRequired: widget.existingPaymentMethodRequired, + ); } 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..4fcd02b1 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 @@ -66,9 +66,11 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity fun buildPaymentProfile( paymentProfileString: String, onlyIncludeFields: List, - paymentItems: List>? = null + paymentItems: List>? = null, + existingPaymentMethodRequired: Boolean = false ): JSONObject { val rawPaymentProfile = JSONObject(paymentProfileString) + rawPaymentProfile.put("existingPaymentMethodRequired", existingPaymentMethodRequired) // Add payment information paymentItems?.find { it["type"] == "total" }?.let { @@ -128,8 +130,9 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity * * @param result callback to communicate back with the Dart end in Flutter. * @param paymentProfileString the payment configuration object in [String] format. + * @param existingPaymentMethodRequired whether the user must have an existing payment method. */ - fun isReadyToPay(result: Result, paymentProfileString: String) { + fun isReadyToPay(result: Result, paymentProfileString: String, existingPaymentMethodRequired: Boolean = false) { // Construct profile and client val paymentProfile = buildPaymentProfile( @@ -139,7 +142,7 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity "apiVersionMinor", "allowedPaymentMethods", "existingPaymentMethodRequired" - ) + ), existingPaymentMethodRequired = existingPaymentMethodRequired ) val client = paymentClientForProfile(paymentProfile) 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..15a5325f 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 existingPaymentMethodRequired = args!!["existingPaymentMethodRequired"] as? Boolean ?: false + googlePayHandler.isReadyToPay(result, paymentProfileString, existingPaymentMethodRequired) + } METHOD_SHOW_PAYMENT_SELECTOR -> { if (eventChannelIsActive) { val arguments = call.arguments>()!! diff --git a/pay_ios/ios/pay_ios/Sources/pay_ios/PayPlugin.swift b/pay_ios/ios/pay_ios/Sources/pay_ios/PayPlugin.swift index 31ae750e..5f4cd40d 100644 --- a/pay_ios/ios/pay_ios/Sources/pay_ios/PayPlugin.swift +++ b/pay_ios/ios/pay_ios/Sources/pay_ios/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, + existingPaymentMethodRequired: args["existingPaymentMethodRequired"] 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/pay_ios/Sources/pay_ios/PaymentHandler.swift b/pay_ios/ios/pay_ios/Sources/pay_ios/PaymentHandler.swift index 887aadf0..205f9936 100644 --- a/pay_ios/ios/pay_ios/Sources/pay_ios/PaymentHandler.swift +++ b/pay_ios/ios/pay_ios/Sources/pay_ios/PaymentHandler.swift @@ -36,26 +36,30 @@ 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 { - if let supportedNetworks = PaymentHandler.supportedNetworks(from: paymentConfiguration) { + /// - parameter existingPaymentMethodRequired: 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, existingPaymentMethodRequired: Bool = true) -> Bool { + if existingPaymentMethodRequired { + guard let supportedNetworks = PaymentHandler.supportedNetworks(from: paymentConfiguration) else { + return false + } return PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks) - } else { - return false } + + return PKPaymentAuthorizationController.canMakePayments() } - + /// 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 +77,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 +95,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 +105,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 +115,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 +129,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 +139,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 +181,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..e5632e44 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 existingPaymentMethodRequired = false}) async { return await _channel.invokeMethod( - 'userCanPay', jsonEncode(await paymentConfiguration.parameterMap())) - as bool; + 'userCanPay', { + 'paymentConfiguration': jsonEncode(await paymentConfiguration.parameterMap()), + 'existingPaymentMethodRequired': existingPaymentMethodRequired, + }, + ) 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..1f2cd79c 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); + /// [existingPaymentMethodRequired]: + /// - If true, only returns true if a supported payment method is available. + /// - If false (default), returns true if the device and user support the payment method, even if none is currently available. + Future userCanPay(PaymentConfiguration paymentConfiguration, {bool existingPaymentMethodRequired = false}); /// Triggers the action to show the payment selector to complete a payment /// with the configuration and a list of [PaymentItem] that help determine diff --git a/pay_platform_interface/test/pay_channel_test.dart b/pay_platform_interface/test/pay_channel_test.dart index 6130e19f..aef42b62 100644 --- a/pay_platform_interface/test/pay_channel_test.dart +++ b/pay_platform_interface/test/pay_channel_test.dart @@ -60,7 +60,12 @@ void main() { await mobilePlatform.userCanPay(dummyConfig); expect( log, - [isMethodCall('userCanPay', arguments: '{}')], + [ + isMethodCall('userCanPay', arguments: { + 'paymentConfiguration': '{}', + 'existingPaymentMethodRequired': false, + }) + ], ); });