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 {
///
/// [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<bool> 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);
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;

/// 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,
Expand All @@ -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.
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,
existingPaymentMethodRequired: widget.existingPaymentMethodRequired,
);
} catch (error) {
widget.onError?.call(error);
rethrow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity
fun buildPaymentProfile(
paymentProfileString: String,
onlyIncludeFields: List<String>,
paymentItems: List<Map<String, Any?>>? = null
paymentItems: List<Map<String, Any?>>? = null,
existingPaymentMethodRequired: Boolean = false
): JSONObject {
val rawPaymentProfile = JSONObject(paymentProfileString)
rawPaymentProfile.put("existingPaymentMethodRequired", existingPaymentMethodRequired)

// Add payment information
paymentItems?.find { it["type"] == "total" }?.let {
Expand Down Expand Up @@ -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(
Expand All @@ -139,7 +142,7 @@ class GooglePayHandler(private val activity: Activity) : PluginRegistry.Activity
"apiVersionMinor",
"allowedPaymentMethods",
"existingPaymentMethodRequired"
)
), existingPaymentMethodRequired = existingPaymentMethodRequired
)

val client = paymentClientForProfile(paymentProfile)
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 existingPaymentMethodRequired = args!!["existingPaymentMethodRequired"] as? Boolean ?: false
googlePayHandler.isReadyToPay(result, paymentProfileString, existingPaymentMethodRequired)
}
METHOD_SHOW_PAYMENT_SELECTOR -> {
if (eventChannelIsActive) {
val arguments = call.arguments<Map<String, Any>>()!!
Expand Down
20 changes: 12 additions & 8 deletions pay_ios/ios/pay_ios/Sources/pay_ios/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,
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)
}
Expand Down
58 changes: 31 additions & 27 deletions pay_ios/ios/pay_ios/Sources/pay_ios/PaymentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<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 +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 {
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 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.
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);
/// [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<bool> 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
Expand Down
7 changes: 6 additions & 1 deletion pay_platform_interface/test/pay_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ void main() {
await mobilePlatform.userCanPay(dummyConfig);
expect(
log,
<Matcher>[isMethodCall('userCanPay', arguments: '{}')],
<Matcher>[
isMethodCall('userCanPay', arguments: <String, Object>{
'paymentConfiguration': '{}',
'existingPaymentMethodRequired': false,
})
],
);
});

Expand Down