Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions pay/lib/src/pay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> 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<bool> 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);
Expand Down
10 changes: 9 additions & 1 deletion pay/lib/src/widgets/pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -136,7 +141,10 @@ class _PayButtonState extends State<PayButton> {

Future<bool> _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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>
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<Map<String, Any>>()!!
Expand Down
20 changes: 12 additions & 8 deletions pay_ios/ios/Classes/PayPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
55 changes: 30 additions & 25 deletions pay_ios/ios/Classes/PaymentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<String> {
paymentRequest.merchantCapabilities = PKMerchantCapability(merchantCapabilities.compactMap { capabilityString in
PKMerchantCapability.fromString(capabilityString)
})
}

// Include the shipping fields required.
if let requiredShippingFields = paymentConfiguration["requiredShippingContactFields"] as? Array<String> {
paymentRequest.requiredShippingContactFields = Set(requiredShippingFields.compactMap { shippingField in
PKContactField.fromString(shippingField)
})
}

// Include the billing fields required.
if let requiredBillingFields = paymentConfiguration["requiredBillingContactFields"] as? Array<String> {
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
}
}
Expand All @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions pay_platform_interface/lib/pay_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> userCanPay(PaymentConfiguration paymentConfiguration) async {
Future<bool> 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.
Expand Down
7 changes: 4 additions & 3 deletions pay_platform_interface/lib/pay_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> 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<bool> 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
Expand Down