<?php
/**
 * WooCommerce Braintree Gateway
 *
 * This source file is subject to the GNU General Public License v3.0
 * that is bundled with this package in the file license.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/gpl-3.0.html
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@woocommerce.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade WooCommerce Braintree Gateway to newer
 * versions in the future. If you wish to customize WooCommerce Braintree Gateway for your
 * needs please refer to http://docs.woocommerce.com/document/braintree/
 *
 * @package   WC-Braintree/Gateway
 * @author    WooCommerce
 * @copyright Copyright: (c) 2016-2020, Automattic, Inc.
 * @license   http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
 */

namespace WC_Braintree;

use Braintree;
use SkyVerge\WooCommerce\PluginFramework\v6_0_1 as Framework;
use SkyVerge\WooCommerce\PluginFramework\v6_0_1\Helpers\OrderHelper;
use WC_Braintree\API\WC_Braintree_API;
use WC_Order;

defined( 'ABSPATH' ) or exit;

/**
 * Braintree Base Gateway Class
 *
 * Handles common functionality among the Credit Card/PayPal gateways
 *
 * @since 2.0.0
 */
class WC_Gateway_Braintree extends Framework\SV_WC_Payment_Gateway_Direct {


	/** Sandbox environment ID */
	const ENVIRONMENT_SANDBOX = 'sandbox';

	/**
	 * The Braintree Auth access token.
	 *
	 * @var string
	 */
	protected $auth_access_token;

	/**
	 * Whether the gateway is connected manually.
	 *
	 * @var bool
	 */
	protected $connect_manually;

	/**
	 * Production merchant ID.
	 *
	 * @var string
	 */
	protected $merchant_id;

	/**
	 * Production public key.
	 *
	 * @var string
	 */
	protected $public_key;

	/**
	 * Production private key.
	 *
	 * @var string
	 */
	protected $private_key;

	/**
	 * Sandbox merchant ID.
	 *
	 * @var string
	 */
	protected $sandbox_merchant_id;

	/**
	 * Sandbox public key.
	 *
	 * @var string
	 */
	protected $sandbox_public_key;

	/**
	 * Sandbox private key.
	 *
	 * @var string
	 */
	protected $sandbox_private_key;

	/**
	 * Name dynamic descriptor.
	 *
	 * @var string
	 */
	protected $name_dynamic_descriptor;

	/**
	 * Phone dynamic descriptor.
	 *
	 * @var string
	 */
	protected $phone_dynamic_descriptor;

	/**
	 * Url dynamic descriptor.
	 *
	 * @var string
	 */
	protected $url_dynamic_descriptor;

	/**
	 * WC_Braintree_API instance.
	 *
	 * @var \WC_Braintree_API
	 */
	protected $api;

	/**
	 * Braintree\Gateway instance.
	 *
	 * @var \Braintree\Gateway
	 */
	protected $sdk;

	/**
	 * Shared settings names.
	 *
	 * @var array
	 */
	protected $shared_settings_names = array( 'environment', 'public_key', 'private_key', 'merchant_id', 'sandbox_public_key', 'sandbox_private_key', 'sandbox_merchant_id', 'name_dynamic_descriptor' );

	/**
	 * Whether this gateway can store credentials.
	 *
	 * Should be overridden to return true for gateways that are permitted to store their own Braintree connection credentials.
	 *
	 * @since 3.7.0
	 * @return bool Whether this gateway instance can store its own Braintree connection credentials.
	 */
	protected function can_gateway_store_credentials(): bool {
		return false;
	}

	/**
	 * Returns a masked version of a private key, showing only the last 4 characters.
	 *
	 * @since 3.11.0
	 *
	 * @param string $key The private key to mask.
	 * @return string The masked key, or empty string if key is empty.
	 */
	public function mask_private_key( string $key ): string {
		if ( '' === $key ) {
			return '';
		}

		$length = strlen( $key );

		if ( $length > 4 ) {
			return str_repeat( self::MASKED_KEY_CHAR, $length - 4 ) . substr( $key, -4 );
		}

		return str_repeat( self::MASKED_KEY_CHAR, $length );
	}

	/**
	 * Checks whether a value is a masked private key placeholder.
	 *
	 * @since 3.11.0
	 *
	 * @param string $value The value to check.
	 * @return bool Whether the value starts with the masked key character.
	 */
	public function is_masked_private_key( string $value ): bool {
		if ( '' === $value ) {
			return false;
		}

		return Framework\SV_WC_Helper::str_starts_with( $value, self::MASKED_KEY_CHAR );
	}

	/**
	 * Stores credentials from all gateway sources for populating read-only fields.
	 *
	 * @since 3.7.0
	 * @var array
	 */
	protected $gateway_credentials = array();

	/**
	 * Character used for masked private key placeholders.
	 *
	 * @since 3.11.0
	 * @var string
	 */
	const MASKED_KEY_CHAR = '•';

	/**
	 * Whether credentials are unavailable for this gateway.
	 *
	 * Set to true when no credential sources are available and gateway cannot use manual credentials.
	 * Used to hide credential fields in get_method_form_fields().
	 *
	 * @since 3.7.0
	 * @var bool
	 */
	protected $no_credentials_available = false;

	/**
	 * Braintree API environment
	 *
	 * @var \WC_Braintree_Payment_Method_Handler
	 */
	protected $auth_environment;

	/**
	 * The remote configuration for the current gateway.
	 *
	 * @var \WC_Braintree\WC_Braintree_Remote_Configuration|null
	 */
	protected $remote_config = null;

	/**
	 * WC_Gateway_Braintree constructor.
	 *
	 * @param string                                 $id the gateway id.
	 * @param Framework\SV_WC_Payment_Gateway_Plugin $plugin the parent plugin class.
	 * @param array                                  $args gateway arguments.
	 */
	public function __construct( $id, $plugin, $args ) {

		parent::__construct( $id, $plugin, $args );

		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
	}

	/**
	 * Enqueues admin scripts.
	 *
	 * @internal
	 *
	 * @since 2.3.11
	 */
	public function enqueue_admin_scripts() {

		if ( $this->get_plugin()->is_plugin_settings() ) {

			wp_enqueue_script( 'wc-backbone-modal', null, [ 'backbone' ] );

			wp_enqueue_script(
				'wc-braintree-admin',
				$this->get_plugin()->get_plugin_url() . '/assets/js/admin/wc-braintree.min.js',
				[ 'jquery', 'wp-i18n' ],
				$this->get_plugin()->get_version(),
				[ 'in_footer' => false ],
			);

			wp_enqueue_style( 'wc-braintree-admin-settings', $this->get_plugin()->get_plugin_url() . '/assets/css/admin/settings.min.css', [], $this->get_plugin()->get_version() );

			// Only localize script for the current gateway's settings page to avoid overwriting params.
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$current_section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '';
			$params          = $this->get_admin_params();

			if ( $current_section === $this->get_id() && ! empty( $params ) ) {
				wp_localize_script( 'wc-braintree-admin', 'wc_braintree_admin_params', $params );
			}
		}
	}


	/**
	 * Gets admin params.
	 *
	 * @internal
	 *
	 * @since 2.5.0
	 * @return array
	 */
	protected function get_admin_params() {
		$merchant_account_configuration_params = $this->get_merchant_account_configuration_params();

		$params = array(
			'merchant_accounts_by_currency' => $merchant_account_configuration_params['merchant_accounts_by_currency'],
			'current_values_by_currency'    => $merchant_account_configuration_params['current_values_by_currency'],
			'gateway_id'                    => $this->get_id(),
			'invalid_merchant_account_text' => esc_html__( 'This merchant account ID is invalid or not available for the selected currency.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			'no_merchant_account_text'      => esc_html__( 'No merchant account ID available for the selected currency.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			'default_label_text'            => esc_html__( '[default]', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			'invalid_label_text'            => esc_html__( '[invalid]', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			/* translators: %s: currency code, e.g. USD, EUR, GBP, AUD */
			'merchant_account_id_title'     => esc_html__( 'Merchant Account ID (%s)', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			'remove_merchant_account_title' => esc_attr__( 'Remove this merchant account ID', 'woocommerce-gateway-paypal-powered-by-braintree' ),
		);

		// Expose credential inheritance data only when this gateway supports shared settings,
		// so the standalone admin script (src/js/admin/wc-braintree.js) can populate read-only
		// fields when the merchant inherits credentials from a sibling gateway.
		if ( ! empty( $this->shared_settings_names ) ) {
			$params['gateway_credentials'] = $this->gateway_credentials;
		}

		return $params;
	}

	/**
	 * Gets the merchant account configuration parameters.
	 *
	 * @return array
	 */
	public function get_merchant_account_configuration_params() {
		$merchant_accounts_by_currency = array();
		$current_values_by_currency    = array();

		try {
			$remote_config = $this->get_remote_config();

			// Get eligible merchant account per supported currency.
			$eligible_merchant_accounts = $remote_config->get_merchant_accounts_by_payment_gateway( $this->get_id() );
			foreach ( $eligible_merchant_accounts as $eligible_merchant_account ) {
				$currency_code = $eligible_merchant_account->get_currency();
				if ( '' !== $currency_code && ! isset( $merchant_accounts_by_currency[ $currency_code ] ) ) {
					$merchant_accounts_by_currency[ $currency_code ] = array();
				}
				$merchant_accounts_by_currency[ $currency_code ][] = [
					'id'         => $eligible_merchant_account->get_id(),
					'is_default' => $eligible_merchant_account->is_default_merchant_account(),
				];

				// Get current saved value for this currency.
				if ( ! isset( $current_values_by_currency[ $currency_code ] ) ) {
					$currency_lower = strtolower( $currency_code );
					$current_value  = $this->get_option( "merchant_account_id_{$currency_lower}" );
					if ( ! empty( $current_value ) ) {
						$current_values_by_currency[ $currency_code ] = $current_value;
					}
				}
			}
		} catch ( \Exception $e ) {
			// If there's an error fetching merchant accounts, continue with empty data.
			\WC_Braintree\Logger::error( 'Failed to fetch merchant accounts for admin params', array( 'error' => $e->getMessage() ) );
			return array(
				'merchant_accounts_by_currency' => array(),
				'current_values_by_currency'    => array(),
			);
		}

		return array(
			'merchant_accounts_by_currency' => $merchant_accounts_by_currency,
			'current_values_by_currency'    => $current_values_by_currency,
		);
	}


	/**
	 * Loads the plugin configuration settings
	 *
	 * @since 2.0.0
	 */
	public function load_settings() {

		parent::load_settings();

		$this->auth_access_token = get_option( 'wc_braintree_auth_access_token', '' );
		$this->auth_environment  = get_option( 'wc_braintree_auth_environment', self::ENVIRONMENT_PRODUCTION );

		// Load shared settings from source gateway if inheriting credentials.
		$this->load_shared_settings();
	}


	/**
	 * Loads any shared settings from the selected source gateway.
	 *
	 * @since 3.7.0
	 */
	protected function load_shared_settings() {

		// Get the selected source gateway ID.
		$source_gateway_id = $this->get_option( 'inherit_settings_source', 'manual' );

		// If manual or no source specified, don't inherit.
		if ( 'manual' === $source_gateway_id || empty( $source_gateway_id ) ) {
			return;
		}

		// Load the source gateway.
		$source_gateway = $this->get_plugin()->get_gateway( $source_gateway_id );

		if ( ! $source_gateway ) {
			return;
		}

		// Copy shared settings from the source gateway.
		foreach ( $this->shared_settings_names as $setting_key ) {
			if ( isset( $source_gateway->$setting_key ) ) {
				$this->$setting_key = $source_gateway->$setting_key;
			}
		}
	}


	/**
	 * Gets the list of gateway IDs that can be used as credential sources.
	 *
	 * @since 3.7.0
	 * @return array Array of gateway IDs.
	 */
	protected function get_available_credential_source_gateways() {

		// Credit Card and PayPal are always potential sources.
		$source_gateways = array(
			WC_Braintree::CREDIT_CARD_GATEWAY_ID,
			WC_Braintree::PAYPAL_GATEWAY_ID,
		);

		// Remove current gateway from the list.
		$source_gateways = array_diff( $source_gateways, array( $this->get_id() ) );

		return $source_gateways;
	}


	/**
	 * Enqueue the Braintree.js library prior to enqueueing gateway scripts
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway::enqueue_scripts()
	 */
	public function enqueue_gateway_assets() {
		$this->register_gateway_assets();

		if ( $this->is_available() ) {
			wp_enqueue_script( 'braintree-js-latinise' );
			wp_enqueue_script( 'braintree-js-client' );

			$utils_asset_path = $this->get_plugin()->get_plugin_path() . '/assets/js/frontend/wc-braintree-utils.asset.php';
			$utils_version    = WC_Braintree::VERSION;
			$utils_deps       = array( 'braintree-js-client' );

			if ( file_exists( $utils_asset_path ) ) {
				$utils_asset   = require $utils_asset_path;
				$utils_version = $utils_asset['version'] ?? $utils_version;
				$utils_deps    = array_merge( $utils_deps, $utils_asset['dependencies'] ?? array() );
			}

			// Note that we only _register_ the utils script here. It will get enqueued by the gateway scripts that need it.
			wp_register_script( 'wc-braintree-utils', $this->get_plugin()->get_plugin_url() . '/assets/js/frontend/wc-braintree-utils.min.js', $utils_deps, $utils_version, true );

			parent::enqueue_gateway_assets();
		}
	}

	/**
	 * Helper function to register the gateway assets without enqueuing them.
	 *
	 * @since 3.8.0
	 * @return void
	 */
	public function register_gateway_assets() {
		if ( ! $this->is_available() ) {
			return;
		}

		wp_register_script( 'braintree-js-latinise', $this->get_plugin()->get_plugin_url() . '/assets/js/frontend/latinise.min.js', array(), WC_Braintree::VERSION, true );
		wp_register_script( 'braintree-js-client', 'https://js.braintreegateway.com/web/' . WC_Braintree::BRAINTREE_JS_SDK_VERSION . '/js/client.min.js', array(), WC_Braintree::VERSION, true );
		wp_register_script( 'braintree-js-data-collector', 'https://js.braintreegateway.com/web/' . WC_Braintree::BRAINTREE_JS_SDK_VERSION . '/js/data-collector.min.js', array( 'braintree-js-client' ), WC_Braintree::VERSION, true );

		$utils_asset_path = $this->get_plugin()->get_plugin_path() . '/assets/js/frontend/wc-braintree-utils.asset.php';
		$utils_version    = WC_Braintree::VERSION;
		$utils_deps       = array( 'braintree-js-client' );

		if ( file_exists( $utils_asset_path ) ) {
			$utils_asset   = require $utils_asset_path;
			$utils_version = $utils_asset['version'] ?? $utils_version;
			$utils_deps    = array_merge( $utils_deps, $utils_asset['dependencies'] ?? array() );
		}
		wp_register_script( 'wc-braintree-utils', $this->get_plugin()->get_plugin_url() . '/assets/js/frontend/wc-braintree-utils.min.js', $utils_deps, $utils_version, true );
	}


	/**
	 * Gets a client authorization token via AJAX.
	 *
	 * @internal
	 *
	 * @since 2.1.0
	 */
	public function ajax_get_client_token() {

		check_ajax_referer( 'wc_' . $this->get_id() . '_get_client_token', 'nonce' );

		try {

			$args = array( 'merchantAccountId' => $this->get_merchant_account_id() );

			// Add domain parameter for Fastlane if enabled on Credit Card gateway.
			if ( WC_Braintree::CREDIT_CARD_GATEWAY_ID === $this->get_id() && $this->is_fastlane_enabled() ) {
				$site_url        = wp_parse_url( home_url(), PHP_URL_HOST );
				$domain          = preg_replace( '/^www\./', '', $site_url );
				$args['domains'] = array( $domain );
			}

			/**
			 * Allow filtering the client token args for custom integrations that may need to pass
			 * additional parameters to Braintree when generating the client token.
			 *
			 * @since 3.11.0
			 *
			 * @param array $args The client token args.
			 * @param WC_Gateway_Braintree $gateway The gateway instance making the request.
			 */
			$args = apply_filters( 'wc_braintree_client_token_args', $args, $this );

			$result = $this->get_api()->get_client_token( $args );

			wp_send_json_success( $result->get_client_token() );

		} catch ( Framework\SV_WC_Plugin_Exception $e ) {

			$this->add_debug_message( $e->getMessage(), 'error' );

			wp_send_json_error(
				array(
					'message' => $e->getMessage(),
				)
			);
		}
	}


	/**
	 * Validate the payment nonce exists
	 *
	 * @since 3.0.0
	 * @param bool $is_valid Whether the nonce is valid.
	 * @return bool
	 */
	public function validate_payment_nonce( $is_valid ) {

		// nonce is required.
		if ( ! Framework\SV_WC_Helper::get_posted_value( 'wc_' . $this->get_id() . '_payment_nonce' ) ) {

			wc_add_notice( esc_html__( 'Oops, there was a temporary payment error. Please try another payment method or contact us to complete your transaction.', 'woocommerce-gateway-paypal-powered-by-braintree' ), 'error' );

			$is_valid = false;
		}

		return $is_valid;
	}


	/**
	 * Add Braintree-specific data to the order prior to processing, currently:
	 *
	 * $order->payment->nonce - payment method nonce
	 * $order->payment->tokenize - true to tokenize payment method, false otherwise
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway_Direct::get_order()
	 * @param int $order order ID being processed.
	 * @return \WC_Order object with payment and transaction information attached
	 */
	public function get_order( $order ) {

		$order = parent::get_order( $order );

		$payment = OrderHelper::get_payment( $order );

		// nonce may be previously populated by Apple Pay.
		if ( empty( $payment->nonce ) ) {
			$payment->nonce = Framework\SV_WC_Helper::get_posted_value( 'wc_' . $this->get_id() . '_payment_nonce' );
		}

		$payment->tokenize = $this->get_payment_tokens_handler()->should_tokenize() || $this->should_tokenize_apple_pay_card() || $this->should_tokenize_google_pay_card();

		// billing address ID if using existing payment token.
		if ( ! empty( $payment->token ) && $this->get_payment_tokens_handler()->user_has_token( $order->get_user_id(), $payment->token ) ) {

			$token = $this->get_payment_tokens_handler()->get_token( $order->get_user_id(), $payment->token );

			if ( $billing_address_id = $token->get_billing_address_id() ) {
				$payment->billing_address_id = $billing_address_id;
			}
		}

		// fraud tool data as a JSON string, unslashed as WP slashes $_POST data which breaks the JSON.
		$payment->device_data = wp_unslash( Framework\SV_WC_Helper::get_posted_value( 'wc_braintree_device_data' ) );

		// merchant account ID.
		if ( $merchant_account_id = $this->get_merchant_account_id( $order->get_currency() ) ) {
			$payment->merchant_account_id = $merchant_account_id;
		}

		// dynamic descriptors.
		$payment->dynamic_descriptors = new \stdClass();

		// only set the name descriptor if it is valid.
		if ( $this->get_name_dynamic_descriptor() && $this->is_name_dynamic_descriptor_valid() ) {
			$payment->dynamic_descriptors->name = $this->get_name_dynamic_descriptor();
		}

		// only set the phone descriptor if it is valid.
		if ( $this->get_phone_dynamic_descriptor() && $this->is_phone_dynamic_descriptor_valid() ) {
			$payment->dynamic_descriptors->phone = $this->get_phone_dynamic_descriptor();
		}

		// the URL descriptor doesn't have any specific validation, so just truncate it if needed.
		$url_dynamic_descriptor            = empty( $this->get_url_dynamic_descriptor() ) ? '' : $this->get_url_dynamic_descriptor();
		$payment->dynamic_descriptors->url = Framework\SV_WC_Helper::str_truncate( $url_dynamic_descriptor, 13, '' );

		// add the recurring flag to Subscriptions renewal orders.
		if ( $this->get_plugin()->is_subscriptions_active() && wcs_order_contains_subscription( $order, 'any' ) ) {

			$payment->subscription             = new \stdClass();
			$payment->subscription->is_renewal = false;

			if ( wcs_order_contains_renewal( $order ) ) {

				$payment->recurring                = true;
				$payment->subscription->is_renewal = true;
			}
		}

		// test amount when in sandbox mode.
		if ( $this->is_test_environment() && ( $test_amount = Framework\SV_WC_Helper::get_posted_value( 'wc-' . $this->get_id_dasherized() . '-test-amount' ) ) ) {
			$payment_total = Framework\SV_WC_Helper::number_format( $test_amount );
			OrderHelper::set_payment_total( $order, $payment_total );
		}

		// Set payment info on the order object.
		OrderHelper::set_payment( $order, $payment );

		return $order;
	}

	/**
	 * Gets the payment data that is submitted by the Apple Pay payment method.
	 *
	 * @since 3.2.0
	 *
	 * @return array
	 */
	public function get_apple_pay_payment_data() {
		$payment_data = sanitize_text_field( wp_unslash( $_POST['payment'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

		if ( ! empty( $payment_data ) ) {
			$payment_data = json_decode( stripslashes( $payment_data ), true );
		} else {
			$payment_data = array();
		}

		return $payment_data;
	}

	/**
	 * Gets the payment data that is submitted by the Google Pay payment method.
	 *
	 * @since 3.4.0
	 *
	 * @return array
	 */
	public function get_google_pay_payment_data(): array {
		$payment_data = sanitize_text_field( wp_unslash( $_POST['paymentData'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

		if ( ! empty( $payment_data ) ) {
			$payment_data = json_decode( $payment_data, true );
		} else {
			$payment_data = array();
		}

		return $payment_data;
	}

	/**
	 * Returns true if the payment method is Apple Pay, false otherwise.
	 *
	 * @since 3.2.0
	 *
	 * @return bool
	 */
	public function is_apple_pay() {
		$payment_data = $this->get_apple_pay_payment_data();

		return isset( $payment_data['source'] ) && 'apple_pay' === $payment_data['source'];
	}

	/**
	 * Returns true if the payment method is Google Pay, false otherwise.
	 *
	 * @since 3.4.0
	 *
	 * @return bool
	 */
	public function is_google_pay(): bool {
		$payment_data = $this->get_google_pay_payment_data();

		return isset( $payment_data['source'] ) && 'google_pay' === $payment_data['source'];
	}

	/**
	 * Determines whether Apple Pay card should be tokenized.
	 *
	 * @since 3.2.0
	 *
	 * @return bool
	 */
	public function should_tokenize_apple_pay_card() {
		if ( ! $this->is_apple_pay() ) {
			return false;
		}

		$payment_data = $this->get_apple_pay_payment_data();

		return isset( $payment_data['force_tokenization'] ) && $payment_data['force_tokenization'];
	}

	/**
	 * Determines whether Google Pay card should be tokenized.
	 *
	 * @since 3.4.0
	 *
	 * @return bool
	 */
	public function should_tokenize_google_pay_card(): bool {
		if ( ! $this->is_google_pay() ) {
			return false;
		}

		$payment_data = $this->get_google_pay_payment_data();

		return isset( $payment_data['force_tokenization'] ) && $payment_data['force_tokenization'];
	}

	/**
	 * Determines whether tokenization should be performed before the sale.
	 *
	 * Most gateways should always tokenize before the sale if the order total is 0.00 (such as a free trial), because
	 * they don't allow 0.00 transactions (but do allow tokenizing without a transaction).
	 *
	 * Gateways that don't support tokenization before the sale (without a transaction) should override this method to
	 * return false, even if order total is 0.00. Note that when doing, so the gateway should also override
	 * `can_tokenize_with_or_after_sale()` to return true.
	 *
	 * Finally, gateways that only tokenize with sale (Moneris), may need to override `should_skip_transaction()` to return false.
	 *
	 * @see SV_WC_Payment_Gateway_Direct::should_tokenize_with_or_after_sale()
	 * @see SV_WC_Payment_Gateway_Direct::can_tokenize_with_or_after_sale()
	 * @see SV_WC_Payment_Gateway_Direct::should_skip_transaction()
	 *
	 * @since 3.2.0
	 *
	 * @param \WC_Order $order the order being paid for.
	 * @return bool
	 */
	protected function should_tokenize_before_sale( WC_Order $order ): bool {
		$tokenize_credit_card     = $this->get_payment_tokens_handler()->should_tokenize();
		$tokenize_apple_pay_card  = $this->should_tokenize_apple_pay_card();
		$tokenize_google_pay_card = $this->should_tokenize_google_pay_card();
		$result                   = ( $tokenize_credit_card || $tokenize_apple_pay_card || $tokenize_google_pay_card ) && ( '0.00' === OrderHelper::get_payment_total( $order ) || $this->tokenize_before_sale() );

		/**
		 * Filters whether tokenization should be performed before the sale, for a given order.
		 *
		 * @see SV_WC_Payment_Gateway_Direct::should_tokenize_before_sale()
		 *
		 * @since 3.2.0
		 *
		 * @param bool $result
		 * @param \WC_Order $order the order being paid for
		 * @param SV_WC_Payment_Gateway_Direct $gateway the gateway instance
		 * @return bool
		 */
		return apply_filters(
			"wc_payment_gateway_{$this->get_id()}_should_tokenize_before_sale",
			$result,
			$order,
			$this
		);
	}

	/**
	 * Determines whether tokenization should be performed after the sale.
	 *
	 * Performs checks to ensure that the gateway supports tokenization, that the order is not a guest order,
	 * that the gateway supports tokenization after the sale, and that the gateway is configured to tokenize after the sale.
	 *
	 * @see SV_WC_Payment_Gateway_Direct::should_tokenize_before_sale()
	 *
	 * @since 3.2.0
	 *
	 * @param \WC_Order $order the order that was paid for.
	 * @return bool
	 */
	protected function should_tokenize_with_or_after_sale( \WC_Order $order ): bool {

		$result = $this->supports_tokenization() &&
				0 !== (int) $order->get_user_id() &&
				( $this->get_payment_tokens_handler()->should_tokenize() || $this->should_tokenize_apple_pay_card() || $this->should_tokenize_google_pay_card() ) &&
				( $this->tokenize_with_sale() || $this->tokenize_after_sale() ) &&
				$this->can_tokenize_with_or_after_sale( $order );

		/**
		 * Filters whether tokenization should be performed with or after the sale, for a given order.
		 *
		 * @see SV_WC_Payment_Gateway_Direct::should_tokenize_with_or_after_sale()
		 *
		 * @since 3.2.0
		 *
		 * @param bool $result
		 * @param \WC_Order $order the order being paid for
		 * @param SV_WC_Payment_Gateway_Direct $gateway the gateway instance
		 * @return bool
		 */
		return apply_filters(
			"wc_payment_gateway_{$this->get_id()}_should_tokenize_with_or_after_sale",
			$result,
			$order,
			$this
		);
	}

	/**
	 * Gets the order object with data added to process a refund.
	 *
	 * Overridden to add the transaction ID to legacy orders since the v1.x
	 * plugin didn't set its own transaction ID meta.
	 *
	 * @see \SV_WC_Payment_Gateway::get_order_for_refund()
	 * @since 2.0.0
	 * @param \WC_Order $order the order object.
	 * @param float     $amount the refund amount.
	 * @param string    $reason the refund reason.
	 * @return \WC_Order
	 */
	public function get_order_for_refund( $order, $amount, $reason ) {

		$order = parent::get_order_for_refund( $order, $amount, $reason );

		$refund = OrderHelper::get_property( $order, 'refund', null, new \stdClass() );

		if ( empty( $refund->trans_id ) ) {

			$refund->trans_id = $order->get_transaction_id( 'edit' );

			// Set refund info on the order object.
			OrderHelper::set_property( $order, 'refund', $refund );
		}

		return $order;
	}


	/**
	 * Gets the capture handler.
	 *
	 * @since 2.2.0
	 *
	 * @return \WC_Braintree\Capture
	 */
	public function get_capture_handler() {
		return new \WC_Braintree\Capture( $this );
	}


	/** Tokenization methods **************************************************/


	/**
	 * Braintree tokenizes payment methods during the transaction (if successful)
	 *
	 * @since 3.0.0
	 * @return bool
	 */
	public function tokenize_with_sale() {
		return true;
	}


	/**
	 * Return the custom Braintree payment tokens handler class
	 *
	 * @since 3.2.0
	 * @return \WC_Braintree_Payment_Method_Handler
	 */
	protected function build_payment_tokens_handler() {

		return new WC_Braintree_Payment_Method_Handler( $this );
	}


	/** Admin settings methods ************************************************/


	/**
	 * Returns an array of form fields specific for this method
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway::get_method_form_fields()
	 * @return array of form fields
	 */
	protected function get_method_form_fields() {

		// Credential fields - only included if credentials are available.
		// The no_credentials_available flag is set in add_shared_settings_form_fields().
		$credential_fields = array();

		if ( ! $this->no_credentials_available ) {
			$credential_fields = array(
				// Production.
				'merchant_id'         => array(
					'title'    => __( 'Merchant ID', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'type'     => 'text',
					'class'    => 'environment-field production-field',
					'desc_tip' => __( 'The Merchant ID for your Braintree account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				),
				'public_key'          => array(
					'title'    => __( 'Public Key', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'type'     => 'text',
					'class'    => 'environment-field production-field',
					'desc_tip' => __( 'The Public Key for your Braintree account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				),
				'private_key'         => array(
					'title'    => __( 'Private Key', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'type'     => 'masked_password',
					'class'    => 'environment-field production-field',
					'desc_tip' => __( 'The Private Key for your Braintree account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				),
				// Sandbox.
				'sandbox_merchant_id' => array(
					'title'    => __( 'Sandbox Merchant ID', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'type'     => 'text',
					'class'    => 'environment-field sandbox-field',
					'desc_tip' => __( 'The Merchant ID for your Braintree sandbox account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				),
				'sandbox_public_key'  => array(
					'title'    => __( 'Sandbox Public Key', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'type'     => 'text',
					'class'    => 'environment-field sandbox-field',
					'desc_tip' => __( 'The Public Key for your Braintree sandbox account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				),
				'sandbox_private_key' => array(
					'title'    => __( 'Sandbox Private Key', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'type'     => 'masked_password',
					'class'    => 'environment-field sandbox-field',
					'desc_tip' => __( 'The Private Key for your Braintree sandbox account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				),
			);
		}

		// Additional form fields - always included.
		$additional_fields = array(
			'webhook_info'               => array(
				'title' => __( 'Webhook Info', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'  => 'webhook_info',
			),
			// Merchant account ID per currency feature.
			'merchant_account_id_title'  => array(
				'title'       => __( 'Merchant Account IDs', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'        => 'title',
				'description' => sprintf(
					/* translators: 1: Opening link tag to documentation. 2: Closing link tag. */
					esc_html__( 'Enter additional merchant account IDs if you do not want to use your Braintree account default. %1$sLearn more about merchant account IDs%2$s', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'<a href="' . esc_url( wc_braintree()->get_documentation_url() ) . '#multicurrency-setup">',
					'&nbsp;&rarr;</a>'
				),
			),
			'merchant_account_id_fields' => array( 'type' => 'merchant_account_ids' ),
			// Dynamic descriptors.
			'dynamic_descriptor_title'   => array(
				'title'       => __( 'Dynamic Descriptors', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'        => 'title',
				/* translators: Placeholders: %1$s - </p> tag (intended to precede the opening tag to account for Settings API markup), %2$s - <p> tag, %3$s - <a> tag, %4$s - </a> tag */
				'description' => sprintf( esc_html__( 'Dynamic descriptors define what will appear on your customers\' credit card statements for a specific purchase. Contact Braintree to enable these for your account.%1$s %2$sPlease ensure that you have %3$sread the documentation on dynamic descriptors%4$s and are using an accepted format.', 'woocommerce-gateway-paypal-powered-by-braintree' ), '</p>', '<p style="font-weight: bold;">', '<a target="_blank" href="https://docs.woocommerce.com/document/woocommerce-gateway-paypal-powered-by-braintree/#dynamic-descriptors-setup">', '</a>' ),
			),
			'name_dynamic_descriptor'    => array(
				'title'             => __( 'Name', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'              => 'text',
				'class'             => 'js-dynamic-descriptor-name',
				'desc_tip'          => __( 'The value in the business name field of a customer\'s statement. Company name/DBA section must be either 3, 7 or 12 characters and the product descriptor can be up to 18, 14, or 9 characters respectively (with an * in between for a total descriptor name of 22 characters).', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'custom_attributes' => array( 'maxlength' => 22 ),
			),
			'phone_dynamic_descriptor'   => array(
				'title'             => __( 'Phone', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'              => 'text',
				'class'             => 'js-dynamic-descriptor-phone',
				'desc_tip'          => __( 'The value in the phone number field of a customer\'s statement. Phone must be exactly 10 characters and can only contain numbers, dashes, parentheses and periods.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'custom_attributes' => array( 'maxlength' => 14 ),
			),
			'url_dynamic_descriptor'     => array(
				'title'             => __( 'URL', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'              => 'text',
				'class'             => 'js-dynamic-descriptor-url',
				'desc_tip'          => __( 'The value in the URL/web address field of a customer\'s statement. The URL must be 13 characters or less.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'custom_attributes' => array( 'maxlength' => 13 ),
			),
		);

		return array_merge( $credential_fields, $additional_fields );
	}


	/**
	 * Adds the shared settings form fields.
	 *
	 * @since 2.0.0
	 *
	 * @param array $form_fields
	 * @return array
	 */
	protected function add_shared_settings_form_fields( $form_fields ) {

		$this->load_settings();

		// Save and remove environment field to add it after connection settings.
		$environment_field = null;
		if ( isset( $form_fields['environment'] ) ) {
			$environment_field = $form_fields['environment'];
			unset( $form_fields['environment'] );
		}

		// Add connection settings title.
		$form_fields['connection_settings'] = array(
			'title' => esc_html__( 'Connection Settings', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			'type'  => 'title',
		);

		// Build options for the credentials source dropdown.
		$source_options            = array();
		$this->gateway_credentials = array(); // Store credentials for populating read-only fields.

		// Add manual option if this gateway allows it.
		if ( $this->can_gateway_store_credentials() ) {
			$source_options['manual'] = esc_html__( 'Manual credentials', 'woocommerce-gateway-paypal-powered-by-braintree' );
		}

		// Add available gateway sources.
		$available_gateways = $this->get_available_credential_source_gateways();

		foreach ( $available_gateways as $gateway_id ) {
			$gateway_settings = $this->get_plugin()->get_gateway_settings( $gateway_id );

			// Skip gateways that are themselves inheriting from another gateway.
			// Only allow inheriting from gateways with manual credentials to avoid circular dependencies.
			$inherit_source = isset( $gateway_settings['inherit_settings_source'] ) ? $gateway_settings['inherit_settings_source'] : 'manual';
			if ( 'manual' !== $inherit_source ) {
				continue;
			}

			// Check which environments this gateway has credentials for.
			$has_production = ! empty( $gateway_settings['merchant_id'] )
				&& ! empty( $gateway_settings['public_key'] )
				&& ! empty( $gateway_settings['private_key'] );

			$has_sandbox = ! empty( $gateway_settings['sandbox_merchant_id'] )
				&& ! empty( $gateway_settings['sandbox_public_key'] )
				&& ! empty( $gateway_settings['sandbox_private_key'] );

			// Only add gateway if it has credentials for at least one environment.
			if ( $has_production || $has_sandbox ) {
				$gateway = $this->get_plugin()->get_gateway( $gateway_id );

				$source_options[ $gateway_id ] = sprintf(
					/* translators: %s: Gateway title */
					esc_html__( 'Use credentials from %s', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					$gateway->get_method_title()
				);

				// Store credentials for this gateway.
				$this->gateway_credentials[ $gateway_id ] = array(
					'environment'         => isset( $gateway_settings['environment'] ) ? $gateway_settings['environment'] : 'production',
					'merchant_id'         => isset( $gateway_settings['merchant_id'] ) ? $gateway_settings['merchant_id'] : '',
					'public_key'          => isset( $gateway_settings['public_key'] ) ? $gateway_settings['public_key'] : '',
					'private_key'         => $this->mask_private_key( isset( $gateway_settings['private_key'] ) ? $gateway_settings['private_key'] : '' ),
					'sandbox_merchant_id' => isset( $gateway_settings['sandbox_merchant_id'] ) ? $gateway_settings['sandbox_merchant_id'] : '',
					'sandbox_public_key'  => isset( $gateway_settings['sandbox_public_key'] ) ? $gateway_settings['sandbox_public_key'] : '',
					'sandbox_private_key' => $this->mask_private_key( isset( $gateway_settings['sandbox_private_key'] ) ? $gateway_settings['sandbox_private_key'] : '' ),
				);
			}
		}

		// Only show dropdown if there are options.
		if ( ! empty( $source_options ) ) {
			// Determine default source.
			$default_source = $this->can_gateway_store_credentials() ? 'manual' : key( $source_options );

			$form_fields['inherit_settings_source'] = array(
				'title'       => $this->can_gateway_store_credentials()
					? esc_html__( 'Credentials source', 'woocommerce-gateway-paypal-powered-by-braintree' )
					: esc_html__( 'Use credentials from', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'        => 'select',
				'class'       => 'wc-enhanced-select',
				'description' => $this->can_gateway_store_credentials()
					? esc_html__( 'Choose whether to enter Braintree API credentials manually or use credentials from another gateway.', 'woocommerce-gateway-paypal-powered-by-braintree' )
					: esc_html__( "Select which gateway's Braintree API credentials to use.", 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'default'     => $default_source,
				'options'     => $source_options,
			);
		} elseif ( ! $this->can_gateway_store_credentials() ) {
			// No source options available and this gateway cannot use manual credentials.
			// Show a warning and flag to hide credential fields in get_method_form_fields().
			$this->no_credentials_available = true;

			$credit_card_settings_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . WC_Braintree::CREDIT_CARD_GATEWAY_ID );
			$paypal_settings_url      = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . WC_Braintree::PAYPAL_GATEWAY_ID );

			$form_fields['no_credentials_notice'] = array(
				'title'       => esc_html__( 'Credentials Required', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'type'        => 'title',
				'description' => sprintf(
					/* translators: %1$s - opening strong tag, %2$s - closing strong tag, %3$s - opening Credit Card link tag, %4$s - closing link tag, %5$s - opening PayPal link tag */
					esc_html__( '%1$sNo Braintree credentials available.%2$s Please configure your Braintree API credentials in the %3$sCredit Card%4$s or %5$sPayPal%4$s gateway settings first. This gateway will use the credentials from one of those gateways.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
					'<strong>',
					'</strong>',
					'<a href="' . esc_url( $credit_card_settings_url ) . '">',
					'</a>',
					'<a href="' . esc_url( $paypal_settings_url ) . '">'
				),
			);
		}

		// Add environment field after credentials source (if it was saved earlier).
		// Skip if no credentials are available (no source options and can't use manual credentials).
		if ( $environment_field && ! $this->no_credentials_available ) {
			$form_fields['environment'] = $environment_field;
		}

		// Handle Braintree Auth if this gateway supports it.
		if ( $this->can_connect() ) {

			// only show this option when connected via auth flow.
			if ( $this->is_connected() && ! $this->is_connected_manually() ) {

				$form_fields = Framework\SV_WC_Helper::array_insert_after(
					$form_fields,
					'connection_settings',
					[
						'braintree_auth'   => [
							/** Field type. @see \WC_Gateway_Braintree::generate_braintree_auth_html() */
							'type' => 'braintree_auth',
						],
						'connect_manually' => [
							'type'    => 'checkbox',
							'label'   => __( 'Enter connection credentials manually', 'woocommerce-gateway-paypal-powered-by-braintree' ),
							'default' => 'no',
						],
					]
				);
			} else {

				$this->connect_manually = 'yes';
			}
		}

		return $form_fields;
	}


	/**
	 * Generates the Braintree Auth connection HTML.
	 *
	 * This method will be phased out as the manual connection is the preferred setup method.
	 *
	 * @see \WC_Gateway_Braintree::add_shared_settings_form_fields()
	 *
	 * @internal
	 *
	 * @since 2.0.0
	 * @deprecated since 2.3.11
	 *
	 * @return string HTML
	 */
	public function generate_braintree_auth_html() {

		// no long connect via auth for new merchants or merchants that have already connected manually.
		if ( ! $this->is_connected() || $this->is_connected_manually() ) {
			return '';
		}

		ob_start();

		?>
		<tr class="wc-braintree-auth">
			<th>
				<?php

				esc_html_e( 'Connect/Disconnect', 'woocommerce-gateway-paypal-powered-by-braintree' );

				echo wc_help_tip( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					sprintf( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						'%s<br><br>%s<br><br>%s',
						__( 'You just connected your PayPal Enterprise Payments account to WooCommerce. You can start taking payments now.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
						__( 'Once you have processed a payment, PayPal will review your application for final approval. Before you ship any goods make sure you have received a final approval for your PayPal Enterprise Payments account.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
						__( 'Questions? We are a phone call away: 1-855-489-0345.', 'woocommerce-gateway-paypal-powered-by-braintree' )
					)
				);

				?>
			</th>
			<td>
				<a
					href="<?php echo esc_url( $this->get_disconnect_url() ); ?>"
					id="wc-braintree-auth-disconnect"
					class="button-primary"
				>
				<?php
					echo esc_html__( 'Disconnect from PayPal Enterprise Payments', 'woocommerce-gateway-paypal-powered-by-braintree' );
				?>
				</a>
			</td>
		</tr>

		<script type="text/template" id="tmpl-wc-braintree-auth-disconnect-modal">
			<div class="wc-backbone-modal wc-braintree-auth-disconnect-modal">
				<div class="wc-backbone-modal-content">
					<section class="wc-backbone-modal-main" role="main">
						<header class="wc-backbone-modal-header">
							<h1><?php esc_html_e( 'PayPal Enterprise Payments', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?></h1>
							<button class="modal-close modal-close-link dashicons dashicons-no-alt">
								<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel and cancel', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?></span>
							</button>
						</header>
						<article>
							<p>
							<?php
							printf(
								/* translators: Placeholders %1$s - opening HTML <a> link tag, closing HTML </a> link tag */
								esc_html__( 'Heads up! Once you disconnect, you\'ll need to use your %1$sBraintree API keys%2$s to reconnect. Do you want to proceed with disconnecting?', 'woocommerce-gateway-paypal-powered-by-braintree' ),
								'<a href="https://docs.woocommerce.com/document/woocommerce-gateway-paypal-powered-by-braintree/#setup" target="_blank">',
								'</a>'
							);
							?>
							</p>
						</article>
						<footer style="text-align: right;">
							<button
								class="button"
							><?php esc_html_e( 'Cancel', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?></button>
							<a
								href="<?php echo esc_url( $this->get_disconnect_url() ); ?>"
								class="button button-primary"
							><?php esc_html_e( 'Disconnect', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?></a>
						</footer>
					</section>
				</div>
			</div>
			<div class="wc-backbone-modal-backdrop modal-close"></div>
		</script>
		<?php

		$field = ob_get_clean();

		ob_start();
		?>
		( function( $ ) {
			$( function() {
				$( '#wc-braintree-auth-disconnect' ).on( 'click', function( e ) {
					e.preventDefault();

					$( '#wc-backbone-modal-dialog .modal-close' ).trigger( 'click' );

					new $.WCBackboneModal.View( {
						target: 'wc-braintree-auth-disconnect-modal'
					} );

					$( '.wc-braintree-auth-disconnect-modal .button' ).on( 'click', function( e ) {
						if ( ! $( this ).hasClass( 'button-primary' ) ) {
							$( '.wc-braintree-auth-disconnect-modal button.modal-close' ).trigger( 'click' );
						}
					} );
				} );
			} );
		} )( jQuery );
		<?php
		$javascript = ob_get_clean();
		WC_Braintree::enqueue_inline_script( 'wc-braintree-auth-disconnect', $javascript );

		return $field;
	}


	/**
	 * Generates HTML for the masked_password field type.
	 *
	 * Renders a text input with the stored value replaced by a masked placeholder
	 * to prevent private key exposure in page source.
	 *
	 * @since 3.11.0
	 *
	 * @param string $key  Field key.
	 * @param array  $data Field data.
	 * @return string The field HTML.
	 */
	public function generate_masked_password_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data  = wp_parse_args( $data, $defaults );
		$value = $this->mask_private_key( $this->get_option( $key ) );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>">
					<?php echo wp_kses_post( $data['title'] ); ?>
					<?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<input
						class="input-text regular-input <?php echo esc_attr( $data['class'] ); ?>"
						type="text"
						name="<?php echo esc_attr( $field_key ); ?>"
						id="<?php echo esc_attr( $field_key ); ?>"
						style="<?php echo esc_attr( $data['css'] ); ?>"
						value="<?php echo esc_attr( $value ); ?>"
						placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>"
						<?php disabled( $data['disabled'], true ); ?>
						<?php echo $this->get_custom_attribute_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					/>
					<?php echo $this->get_description_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}


	/**
	 * Validates a masked_password field value during save.
	 *
	 * If the submitted value is a masked placeholder (unchanged by the user),
	 * returns the existing stored value. Otherwise returns the new value as-is.
	 *
	 * @since 3.11.0
	 *
	 * @param string $key   Field key.
	 * @param string $value The submitted field value.
	 * @return string The value to save.
	 */
	public function validate_masked_password_field( $key, $value ) {
		$value = is_null( $value ) ? '' : wc_clean( $value );

		if ( $this->is_masked_private_key( $value ) ) {
			return $this->get_option( $key );
		}

		return $value;
	}


	/**
	 * Gets the Braintree Auth connect URL.
	 *
	 * Although the Partner API expects an array, the WooCommerce Connect
	 * middleware presently wants things flattened. So instead of passing a user
	 * array and a business array, we pass selected fields with `user_` and
	 * `business_` prepended.
	 *
	 * @since 2.0.0
	 * @param string $environment the desired environment, either 'production' or 'sandbox'.
	 * @return string
	 */
	protected function get_connect_url( $environment = self::ENVIRONMENT_PRODUCTION ) {

		$production_connect_url = 'https://connect.woocommerce.com/login/braintree';
		$sandbox_connect_url    = 'https://connect.woocommerce.com/login/braintreesandbox';

		$redirect_url = add_query_arg( 'wc_paypal_braintree_admin_nonce', wp_create_nonce( 'connect_paypal_braintree' ), $this->get_plugin()->get_payment_gateway_configuration_url( $this->get_id() ) );
		$current_user = wp_get_current_user();

		// Note:  We doubly urlencode the redirect url to avoid Braintree's server
		// decoding it which would cause loss of query params on the final redirect.
		$query_args = array(
			'user_email'        => $current_user->user_email,
			'business_currency' => get_woocommerce_currency(),
			'business_website'  => get_bloginfo( 'url' ),
			'redirect'          => urlencode( urlencode( $redirect_url ) ),
			'scopes'            => 'read_write',
		);

		if ( ! empty( $current_user->user_firstname ) ) {
			$query_args['user_firstName'] = $current_user->user_firstname;
		}

		if ( ! empty( $current_user->user_lastname ) ) {
			$query_args['user_lastName'] = $current_user->user_lastname;
		}

		// Let's go ahead and assume the user and business are in the same region and country,
		// because they probably are.  If not, they can edit these anyways.
		$base_location = wc_get_base_location();

		if ( ! empty( $base_location['country'] ) ) {
			$query_args['business_country'] = $query_args['user_country'] = $base_location['country'];
		}

		if ( ! empty( $base_location['state'] ) ) {
			$query_args['business_region'] = $query_args['user_region'] = $base_location['state'];
		}

		$site_name = \WC_Braintree\WC_Braintree::get_braintree_store_name();
		if ( $site_name ) {
			$query_args['business_name'] = $site_name;
		}

		if ( $site_description = get_bloginfo( 'description' ) ) {
			$query_args['business_description'] = $site_description;
		}

		if ( self::ENVIRONMENT_SANDBOX === $environment ) {
			$connect_url = 'https://connect.woocommerce.com/login/braintreesandbox';
		} else {
			$connect_url = 'https://connect.woocommerce.com/login/braintree';
		}

		return esc_url( add_query_arg( $query_args, $connect_url ) );
	}


	/**
	 * Gets the Braintree Auth disconnect URL.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	protected function get_disconnect_url() {

		$url = add_query_arg( 'disconnect_paypal_braintree', 1, $this->get_plugin()->get_payment_gateway_configuration_url( $this->get_id() ) );

		return wp_nonce_url( $url, 'disconnect_paypal_braintree', 'wc_paypal_braintree_admin_nonce' );
	}


	/** Merchant account ID (multi-currency) feature **************************/


	/**
	 * Generate the merchant account ID section HTML, including the currency
	 * selector and any existing merchant account IDs that have been entered
	 * by the admin
	 *
	 * @since 3.0.0
	 * @return string HTML
	 */
	protected function generate_merchant_account_ids_html() {

		$base_currency = get_woocommerce_currency();

		/* translators: %s: currency code */
		$button_text = sprintf( __( 'Add merchant account ID for %s', 'woocommerce-gateway-paypal-powered-by-braintree' ), $base_currency );

		// currency selector.
		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<?php esc_html_e( 'Add merchant account ID', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>
			</th>
			<td class="forminp">
				<select id="wc_braintree_merchant_account_id_currency" class="wc-enhanced-select">
					<?php foreach ( get_woocommerce_currencies() as $code => $name ) : ?>
						<option <?php selected( $code, $base_currency ); ?> value="<?php echo esc_attr( $code ); ?>">
							<?php echo esc_html( sprintf( '%s (%s)', $name, get_woocommerce_currency_symbol( $code ) ) ); ?>
						</option>
					<?php endforeach; ?>
				</select>
				<p class="wc-braintree-merchant-account-button-row">
					<a href="#" class="button js-add-merchant-account-id"><?php echo esc_html( $button_text ); ?></a>
					<span class="spinner wc-braintree-merchant-account-loader"></span>
				</p>
			</td>
		</tr>
		<?php

		$html = ob_get_clean();
		// generate HTML for saved merchant account IDs.
		foreach ( array_keys( $this->settings ) as $key ) {
			if ( preg_match( '/merchant_account_id_([a-z]{3})$/', $key, $matches ) ) {
				$currency      = $matches[1];
				$current_value = $this->get_option( "merchant_account_id_{$currency}" );

				// Only generate HTML if there's a saved value (not empty).
				if ( ! empty( $current_value ) ) {
					$html .= $this->generate_merchant_account_id_html( $currency );
				}
			}
		}

		return $html;
	}


	/**
	 * Generate the webhook info section HTML
	 *
	 * @since 3.5.0
	 * @return string HTML
	 */
	protected function generate_webhook_info_html() {
		$webhook_url       = $this->get_webhook_url();
		$documentation_url = 'https://developer.paypal.com/braintree/docs/guides/webhooks/create/php/';

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<?php esc_html_e( 'Webhook URL', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>
			</th>
			<td class="forminp">
				<div class="wc-braintree-webhook-info">
						<div class="wc-braintree-webhook-url-container" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px;">
							<input type="text" class="wc-braintree-webhook-url-input" value="<?php echo esc_attr( $webhook_url ); ?>" readonly />
							<button
								type="button"
								class="button wc-braintree-copy-webhook-url"
								onclick="navigator.clipboard.writeText('<?php echo esc_js( $webhook_url ); ?>').then(() => this.textContent = '<?php esc_attr_e( 'Copied!', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>'); setTimeout(() => this.textContent = '<?php esc_attr_e( 'Copy', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>', 2000);"
							>
								<?php esc_html_e( 'Copy', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>
							</button>
						</div>
						<p class="description">
							<?php esc_html_e( 'This is the URL that Braintree uses to send notifications for order status updates and payment processing.', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>
						</p>
						<p class="description">
							<?php
							printf(
								/* translators: 1: Opening link tag to webhooks documentation. 2: Closing link tag. */
								esc_html__( 'You can configure it in your Braintree Dashboard. %1$sLearn more%2$s', 'woocommerce-gateway-paypal-powered-by-braintree' ),
								'<a target="_blank" href="' . esc_url( $documentation_url ) . '">',
								'&nbsp;&rarr;</a>'
							);
							?>
						</p>
				</div>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}


	/**
	 * Display the settings page with some additional CSS/JS to support the
	 * merchant account IDs feature
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway::admin_options()
	 */
	public function admin_options() {

		parent::admin_options();

		?>
		<style type="text/css">

			.js-remove-merchant-account-id .dashicons-trash { margin-top: 5px; opacity: .4; } .js-remove-merchant-account-id { text-decoration: none; }
			input.js-dynamic-descriptor-valid { border-color: #7ad03a; } input.js-dynamic-descriptor-invalid { border-color: #a00; }

			.wc-braintree-auth.disabled {
				opacity: 0.25;
			}
			.wc-braintree-auth.disabled .wc-braintree-connect-button {
				cursor: default;
			}

			.wc-braintree-webhook-info code {
				background: #f1f1f1;
				padding: 2px 4px 4px 0;
				border-radius: 3px;
			}

			.wc-braintree-webhook-info .wc-braintree-webhook-url-input {
				flex: 1;
				padding: 8px;
				border: 1px solid #ddd;
				border-radius: 4px;
				background-color: #f9f9f9;
				font-family: monospace;
				font-size: 13px;
			}

			.wc-braintree-webhook-info .wc-braintree-copy-webhook-url {
				white-space: nowrap;
			}

		</style>

		<?php ob_start(); ?>
		( function( $ ) {
			$( function() {
				$( document.body ).on( 'click', '.wc-braintree-auth.disabled .wc-braintree-connect-button', function( e ) {
					e.preventDefault();
				} );

				<?php
				// hide the "manually connect" toggle if already connected via Braintree Auth.
				if ( $this->is_connected() ) :
					?>
					$( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_connect_manually' ).closest( 'tr' ).hide();
				<?php endif; ?>

				$( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_connect_manually' ).change( function() {

			var $environment = $( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_environment' ).val();

			var $environmentFields = $( '.' + $environment + '-field' );

			if ( $( this ).is( ':checked' ) ) {

				$( 'tr.wc-braintree-auth' ).addClass( 'disabled' );

				$( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_environment' ).closest( 'tr' ).show();
				$( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_inherit_settings' ).closest( 'tr' ).show();

				if ( ! $( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_inherit_settings' ).is( ':checked' ) ) {
					$environmentFields.closest( 'tr' ).show();
				}

			} else {

				$( 'tr.wc-braintree-auth' ).removeClass( 'disabled' );

				$( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_environment' ).closest( 'tr' ).hide();
				$( '#woocommerce_<?php echo esc_js( $this->get_id() ); ?>_inherit_settings' ).closest( 'tr' ).hide();

				$environmentFields.closest( 'tr' ).hide();
			}

		} ).change();

		// sync add merchant account ID button text to selected currency
		$( 'select#wc_braintree_merchant_account_id_currency' ).change( function() {
			$( '.js-add-merchant-account-id' ).text( '<?php esc_html_e( 'Add merchant account ID for ', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>' + $( this ).val() )
		} );

		/**
		 * Render merchant account HTML for a given currency.
		 *
		 * @param {string} currencyCode - The currency code (e.g., 'USD')
		 * @return {HTMLTableRowElement} TR element for the merchant account row.
		 */
		function getMerchantAccountRow( currencyCode ) {
			var params = wc_braintree_admin_params || {};
			var gatewayId = params.gateway_id;
			var currencyLower = currencyCode.toLowerCase();
			var currencyUpper = currencyCode.toUpperCase();
			var id = 'woocommerce_' + gatewayId + '_merchant_account_id_' + currencyLower;
			var name = 'woocommerce_' + gatewayId + '_merchant_account_id[' + currencyLower + ']';
			var title = wp.i18n.sprintf( params.merchant_account_id_title, currencyUpper );
			var merchantAccounts = params.merchant_accounts_by_currency[ currencyCode ] || [];
			var currentValue = params.current_values_by_currency[ currencyCode ] || '';
			var hasAccounts = merchantAccounts.length > 0;
			var isInvalid = currentValue && ! merchantAccounts.some( function( account ) {
				return account.id === currentValue;
			} );

			const trElement = document.createElement( 'tr' );
			trElement.setAttribute( 'valign', 'top' );

			const thElement = document.createElement( 'th' );
			thElement.setAttribute( 'scope', 'row' );
			thElement.className = 'titledesc';

			trElement.appendChild( thElement );

			const labelElement = document.createElement( 'label' );
			labelElement.htmlFor = id;
			labelElement.innerText = title;

			thElement.appendChild( labelElement );

			const tdElement = document.createElement( 'td' );
			tdElement.className = 'forminp wc-braintree-merchant-account-row';

			trElement.appendChild( tdElement );

			const fieldsetElement = document.createElement( 'fieldset' );
			tdElement.appendChild( fieldsetElement );

			const legendElement = document.createElement( 'legend' );
			fieldsetElement.appendChild( legendElement );
			legendElement.className = 'screen-reader-text';

			const legendSpanElement = document.createElement( 'span' );
			legendElement.appendChild( legendSpanElement );
			legendSpanElement.innerText = title;

			const containerDivElement = document.createElement( 'div' );
			containerDivElement.className = 'wc-braintree-merchant-account-container';
			fieldsetElement.appendChild( containerDivElement );

			if ( ! hasAccounts ) {
				const errorDivElement = document.createElement( 'div' );
				containerDivElement.appendChild( errorDivElement );

				const errorInputElement = document.createElement( 'input' );
				errorInputElement.className = 'input-text regular-input js-merchant-account-id-input' + ( isInvalid ? ' error' : '' );
				errorInputElement.type = 'text';
				errorInputElement.name = name;
				errorInputElement.id = id;
				errorInputElement.value = currentValue;
				errorInputElement.placeholder = '';
				errorInputElement.disabled = true;
				errorDivElement.appendChild( errorInputElement );

				const errorParagraphElement = document.createElement( 'p' );
				errorParagraphElement.className = 'description wc-braintree-merchant-account-error';
				errorParagraphElement.innerText = isInvalid ? params.invalid_merchant_account_text : params.no_merchant_account_text;
				errorDivElement.appendChild( errorParagraphElement );
			} else {
				const controlsDivElement = document.createElement( 'div' );
				containerDivElement.appendChild( controlsDivElement );

				const selectElement = document.createElement( 'select' );
				controlsDivElement.appendChild( selectElement );

				selectElement.className = 'wc-enhanced-select js-merchant-account-id-input js-merchant-account-id-select' + ( isInvalid ? ' error' : '' );
				selectElement.name = name;
				selectElement.id = id;
				if ( isInvalid ) {
					selectElement.dataset.invalidValue = currentValue;

					if ( currentValue ) {
						const invalidOptionElement = document.createElement( 'option' );
						selectElement.appendChild( invalidOptionElement );

						invalidOptionElement.value = currentValue;
						invalidOptionElement.innerText = currentValue + ' ' + params.invalid_label_text;
						invalidOptionElement.selected = true;
					}
				}

				merchantAccounts.forEach( function( account ) {
					const optionElement = document.createElement( 'option' );
					selectElement.appendChild( optionElement );

					optionElement.value = account.id;
					optionElement.innerText = account.id + ( account.is_default ? ' ' + params.default_label_text : '' );
					optionElement.selected = ( currentValue === account.id );
				} );

				const invalidPElement = document.createElement( 'p' );
				controlsDivElement.appendChild( invalidPElement );

				invalidPElement.className = 'description wc-braintree-merchant-account-error';
				invalidPElement.style.display = isInvalid ? 'block' : 'none';
				invalidPElement.innerText = params.invalid_merchant_account_text;
			}

			const removeAccountLinkElement = document.createElement( 'a' );
			containerDivElement.appendChild( removeAccountLinkElement );

			removeAccountLinkElement.href = '#';
			removeAccountLinkElement.title = params.remove_merchant_account_title;
			removeAccountLinkElement.className = 'js-remove-merchant-account-id';

			const iconSpanElement = document.createElement( 'span' );
			iconSpanElement.className = 'dashicons dashicons-trash';
			removeAccountLinkElement.appendChild( iconSpanElement );

			return trElement;
		}

		// add new merchant account ID field
		$( '.js-add-merchant-account-id' ).click( function( e ) {
			e.preventDefault();
			var currency = $( 'select#wc_braintree_merchant_account_id_currency' ).val();
			var $button = $( this );

			if ( ! currency ) {
				return;
			}

			// Check if this currency already has a merchant account field
			var fieldName = 'woocommerce_' + wc_braintree_admin_params.gateway_id + '_merchant_account_id[' + currency.toLowerCase() + ']';
			if ( $( 'input[name="' + fieldName + '"], select[name="' + fieldName + '"]' ).length ) {
				return;
			}

			// Render and inject HTML
			const trElement = getMerchantAccountRow( currency );

			// Find the sibling element.
			let siblingDomElement = null;
			if ( $( '.js-merchant-account-id-input' ).length ) {
				siblingDomElement = $( '.js-merchant-account-id-input' ).closest( 'tr' ).last().get( 0 );
			} else {
				siblingDomElement = $button.closest( 'tr' ).get( 0 );
			}
			// Insert the new row.
			siblingDomElement?.parentElement?.appendChild( trElement );

			// Initialize the select if it's a dropdown
			if ( $( trElement ).find( 'select.wc-enhanced-select' ).length ) {
				$( trElement ).find( 'select.wc-enhanced-select' ).selectWoo();
			}
		} );

		// delete existing merchant account ID
		$( '.form-table' ).on( 'click', '.js-remove-merchant-account-id', function( e ) {
			e.preventDefault();

			$( this ).closest( 'tr' ).delay( 50 ).fadeOut( 400, function() {
				$( this ).remove();
			} );
		} );

		// Show/hide error message based on selected merchant account value
		function toggleMerchantAccountError( $select ) {
			var $errorMessage = $select.siblings( '.wc-braintree-merchant-account-error' );
			if ( ! $errorMessage.length ) {
				return;
			}

			var invalidValue = $select.data( 'invalid-value' )?.toString();
			var selectedValue = $select.val()?.toString();

			if ( invalidValue && selectedValue === invalidValue ) {
				// Selected value is invalid, show error message
				$errorMessage.show();
				$select.addClass( 'error' );
			} else {
				// Selected value is valid, hide error message
				$errorMessage.hide();
				$select.removeClass( 'error' );
			}
		}

		// Handle change event for merchant account selects.
		$( '.form-table' ).on( 'change', 'select.js-merchant-account-id-select', function() {
			toggleMerchantAccountError( $( this ) );
		} );

		$( '#woocommerce_braintree_credit_card_name_dynamic_descriptor' ).after( '<span style="margin-top:4px;" class="dashicons dashicons-yes js-dynamic-descriptor-icon"></span>' );

		// company name/DBA dynamic descriptor validation
		$( '#woocommerce_braintree_credit_card_name_dynamic_descriptor' ).on( 'change paste keyup', function () {

			var descriptor = $( this ).val();
			var $icon      = $( '.js-dynamic-descriptor-icon' );

			// not using descriptors
			if ( '' === descriptor ) {
				return;
			}

			// missing asterisk
			if ( -1 === descriptor.indexOf( '*' ) ) {
				$icon.addClass( 'dashicons-no-alt' ).removeClass( 'dashicons-yes' );
				$( this ).addClass( 'js-dynamic-descriptor-invalid' ).removeClass( 'js-dynamic-descriptor-valid' );
				return;
			}

			descriptor = descriptor.split( '*', 2 );
			name       = descriptor[0];
			product    = descriptor[1];

			// company name must be 3, 7, or 12 characters
			if ( 3 !== name.length && 7 !== name.length && 12 !== name.length ) {
				$icon.addClass( 'dashicons-no-alt' ).removeClass( 'dashicons-yes' );
				$( this ).addClass( 'js-dynamic-descriptor-invalid' ).removeClass( 'js-dynamic-descriptor-valid' );
				return;
			}

			$icon.removeClass( 'dashicons-no-alt' ).addClass( 'dashicons-yes' );
			$( this ).addClass( 'js-dynamic-descriptor-valid' ).removeClass( 'js-dynamic-descriptor-invalid' );
		} ).change();
			} );
		} )( jQuery );
		<?php
		$javascript = ob_get_clean();
		WC_Braintree::enqueue_inline_script( 'wc-braintree-admin-settings', $javascript, [ 'jquery', 'wp-i18n' ] );
	}


	/**
	 * Generate HTML for an individual merchant account ID field.
	 * Escapes the HTML before returning it.
	 *
	 * @since 3.0.0
	 * @param string|null $currency_code 3 character currency code for the merchant account ID.
	 * @return string HTML
	 */
	protected function generate_merchant_account_id_html( $currency_code = null ) {

		if ( is_null( $currency_code ) ) {

			// set placeholders to be replaced by JS for new account account IDs.
			$currency_display = '{{currency_display}}';
			$currency_code    = '{{currency_code}}';

		} else {

			// used passed in currency code.
			$currency_display = strtoupper( $currency_code );
			$currency_code    = strtolower( $currency_code );
		}

		$id = sprintf( 'woocommerce_%s_merchant_account_id_%s', $this->get_id(), $currency_code );
		/* translators: %s: currency code, e.g. USD, EUR, GBP, AUD */
		$title = sprintf( __( 'Merchant Account ID (%s)', 'woocommerce-gateway-paypal-powered-by-braintree' ), $currency_display );

		$remote_config     = $this->get_remote_config();
		$merchant_accounts = $remote_config->find_eligible_merchant_accounts_by_currency_and_payment_gateway( $currency_code, $this->get_id() );

		$current_value = $this->get_option( "merchant_account_id_{$currency_code}" );
		$is_invalid    = ! $this->is_current_merchant_account_id_valid( $current_value );

		$invalidity_reason = $is_invalid ? $this->get_current_id_invalidity_reason( $current_value, $currency_code ) : '';

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $id ); ?>"><?php echo esc_html( $title ); ?></label>
			</th>
			<td class="forminp">
				<fieldset>
				<legend class="screen-reader-text"><span><?php echo esc_html( $title ); ?></span></legend>
					<div class="wc-braintree-merchant-account-container">
						<?php if ( count( $merchant_accounts ) === 0 ) : ?>
							<?php
							$input_class = 'input-text regular-input js-merchant-account-id-input';
							if ( $is_invalid ) {
								$input_class .= ' error';
							}
							?>
							<div>
								<input class="<?php echo esc_attr( $input_class ); ?>" type="text" name="<?php printf( 'woocommerce_%s_merchant_account_id[%s]', esc_attr( $this->get_id() ), esc_attr( $currency_code ) ); ?>" id="<?php echo esc_attr( $id ); ?>" value="<?php echo esc_attr( $current_value ); ?>" placeholder="" disabled ?>
								<p class="description wc-braintree-merchant-account-error">
									<?php if ( $is_invalid ) : ?>
										<?php echo esc_html( $invalidity_reason ); ?>
									<?php elseif ( count( $merchant_accounts ) === 0 ) : ?>
										<?php esc_html_e( 'No merchant account ID available for the selected currency.', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>
									<?php endif; ?>
								</p>
							</div>
						<?php else : ?>
							<div>
								<?php
								$select_class = 'wc-enhanced-select js-merchant-account-id-input js-merchant-account-id-select';
								if ( $is_invalid ) {
									$select_class .= ' error';
								}
								?>
								<select class="<?php echo esc_attr( $select_class ); ?>" name="<?php printf( 'woocommerce_%s_merchant_account_id[%s]', esc_attr( $this->get_id() ), esc_attr( $currency_code ) ); ?>" id="<?php echo esc_attr( $id ); ?>" <?php echo $is_invalid && ! empty( $current_value ) ? 'data-invalid-value="' . esc_attr( $current_value ) . '"' : ''; ?>>
									<?php if ( $is_invalid ) : ?>
										<option value="<?php echo esc_attr( $current_value ); ?>" selected="selected"><?php echo esc_html( $current_value ) . ' [invalid]'; ?></option>
									<?php endif; ?>
									<?php
									foreach ( $merchant_accounts as $merchant_account ) :
										$account_id = $merchant_account->get_id();
										$is_default = $merchant_account->is_default_merchant_account();
										$label      = $account_id;

										if ( $is_default ) {
											$label .= esc_html__( ' [default]', 'woocommerce-gateway-paypal-powered-by-braintree' );
										}
										?>
										<option value="<?php echo esc_attr( $account_id ); ?>" <?php selected( $current_value, $account_id ); ?>>
											<?php echo esc_html( $label ); ?>
										</option>
									<?php endforeach; ?>
								</select>
								<p class="description wc-braintree-merchant-account-error" <?php echo $is_invalid ? '' : 'style="display: none;"'; ?>>
									<?php echo esc_html( $invalidity_reason ); ?>
								</p>
							</div>
						<?php endif; ?>
						<a href="#" title="<?php esc_attr_e( 'Remove this merchant account ID', 'woocommerce-gateway-paypal-powered-by-braintree' ); ?>" class="js-remove-merchant-account-id"><span class="dashicons dashicons-trash"></span></a>
					</div>
				</fieldset>
			</td>
		</tr>
		<?php

		// The HTML will not be escaped by whoever is calling this function. So make sure it is escaped before returning.
		// newlines break JS when this HTML is used as a fragment.
		return trim( preg_replace( "/[\n\r\t]/", '', ob_get_clean() ) );
	}

	/**
	 * Check if the current merchant account ID saved in the settings is a valid and supported merchant account for this gateway.
	 *
	 * @param string $current_merchant_account_id The current merchant account ID.
	 * @return bool True if the current merchant account ID is valid, false otherwise.
	 */
	private function is_current_merchant_account_id_valid( $current_merchant_account_id = null ) {
		if ( empty( $current_merchant_account_id ) ) {
			return false;
		}

		$remote_config         = $this->get_remote_config();
		$current_value_account = $remote_config->get_merchant_account( $current_merchant_account_id );

		return null !== $current_value_account;
	}

	/**
	 * Gets the reason why the current merchant account ID is invalid.
	 *
	 * @param string $current_merchant_account_id The current merchant account ID.
	 * @param string $currency The currency to check.
	 * @return string The reason why the current merchant account ID is invalid.
	 */
	private function get_current_id_invalidity_reason( $current_merchant_account_id, $currency ) {
		$default_reason   = __( 'This merchant account ID is invalid or not available for the selected currency.', 'woocommerce-gateway-paypal-powered-by-braintree' );
		$remote_config    = $this->get_remote_config();
		$merchant_account = $remote_config->get_merchant_account( $current_merchant_account_id );

		if ( null === $merchant_account ) {
			return __( 'The merchant account does not exist.', 'woocommerce-gateway-paypal-powered-by-braintree' );
		}

		if ( $merchant_account->get_currency() !== $currency ) {
			/* translators: %1$s: currency code */
			return sprintf( __( 'The merchant account does not support %1$s as a currency.', 'woocommerce-gateway-paypal-powered-by-braintree' ), $currency );
		}

		if ( ! $merchant_account->is_payment_gateway_supported( $this->get_id() ) ) {
			/* translators: %1$s: payment method title */
			return sprintf( __( 'The merchant account does not accept payments for %1$s.', 'woocommerce-gateway-paypal-powered-by-braintree' ), $this->get_method_title() );
		}

		return $default_reason;
	}

	/**
	 * Filter admin options before saving.
	 *
	 * Handles:
	 * - Copying shared settings (credentials, environment) from source gateway when inheriting.
	 * - Dynamically injecting valid merchant account IDs so they're persisted to settings.
	 *
	 * @since 3.0.3 update logic to sanitize multiple merchant account IDs.
	 * @since 3.3.0
	 * @since 3.7.0 Added shared settings inheritance support.
	 * @param array $sanitized_fields Sanitized fields.
	 * @return array
	 */
	public function filter_admin_options( $sanitized_fields ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// remove fields used only for display.
		unset( $sanitized_fields['braintree_auth'] );
		unset( $sanitized_fields['merchant_account_id_title'] );
		unset( $sanitized_fields['merchant_account_ids'] );
		unset( $sanitized_fields['webhook_info'] );
		unset( $sanitized_fields['dynamic_descriptor_title'] );

		// When inheriting settings from another gateway, copy shared settings from the source.
		// This is necessary because disabled form fields are not submitted.
		$source_gateway_id = isset( $sanitized_fields['inherit_settings_source'] ) ? $sanitized_fields['inherit_settings_source'] : 'manual';

		if ( 'manual' !== $source_gateway_id && ! empty( $source_gateway_id ) ) {
			$source_settings = get_option( 'woocommerce_' . $source_gateway_id . '_settings', array() );

			foreach ( $this->shared_settings_names as $setting_key ) {
				if ( isset( $source_settings[ $setting_key ] ) ) {
					$sanitized_fields[ $setting_key ] = $source_settings[ $setting_key ];
				}
			}
		}

		// first unset all merchant account IDs from settings so they can be freshly set.
		foreach ( array_keys( $sanitized_fields ) as $name ) {

			if ( Framework\SV_WC_Helper::str_starts_with( $name, 'merchant_account_id_' ) ) {
				unset( $sanitized_fields[ $name ] );
				unset( $this->settings[ $name ] );
			}
		}

		$merchant_account_id_field_key = sprintf( 'woocommerce_%s_merchant_account_id', $this->get_id() );

		// add merchant account IDs.
		if ( ! empty( $_POST[ $merchant_account_id_field_key ] ) ) {

			$currency_codes = array_keys( get_woocommerce_currencies() );

			// Sanitize merchant account IDs.
			$merchant_account_ids = array_map( 'sanitize_text_field', $_POST[ $merchant_account_id_field_key ] );

			// Filter merchant account IDs to only valid currencies.
			$merchant_account_ids = array_filter(
				$merchant_account_ids,
				static function ( $merchant_account_id, $currency ) use ( $currency_codes ) {
					return in_array( strtoupper( $currency ), $currency_codes, true );
				},
				ARRAY_FILTER_USE_BOTH
			);

			foreach ( $merchant_account_ids as $currency => $merchant_account_id ) {

				// sanity check for valid currency.
				if ( ! in_array( strtoupper( $currency ), $currency_codes, true ) ) {
					continue;
				}

				$merchant_account_key = 'merchant_account_id_' . strtolower( esc_sql( $currency ) );

				// add to persisted fields.
				$sanitized_fields[ $merchant_account_key ] = wp_kses_post( trim( stripslashes( $merchant_account_id ) ) );
				$this->settings[ $merchant_account_key ]   = $sanitized_fields[ $merchant_account_key ];
			}
		}

		return $sanitized_fields;
		// phpcs:enable
	}


	/** Getters ***************************************************************/


	/**
	 * Gets order meta.
	 *
	 * Overridden to account for some straggling meta that may be leftover from
	 * the v1 in certain cases when WC was updated to 3.0 before Subscriptions.
	 *
	 * @since 2.0.2
	 *
	 * @param \WC_Order|int $order order object or ID.
	 * @param string        $key meta key to get.
	 * @return mixed meta value
	 */
	public function get_order_meta( $order, $key ) {

		if ( is_numeric( $order ) ) {
			$order = wc_get_order( $order );
		}

		if ( ! $order ) {
			return false;
		}

		$order_id = $order->get_id();

		// update a legacy payment token if it exists.
		if ( 'payment_token' === $key && $order->meta_exists( '_wc_paypal_braintree_payment_method_token' ) && ! $order->get_meta( $this->get_order_meta_prefix() . $key, true, 'edit' ) && $this->get_id() === $order->get_payment_method( 'edit' ) ) {

			$legacy_token = $order->get_meta( '_wc_paypal_braintree_payment_method_token', true, 'edit' );

			$order->update_meta_data( $this->get_order_meta_prefix() . $key, $legacy_token );
			$order->delete_meta_data( '_wc_paypal_braintree_payment_method_token' );
			$order->save_meta_data();

			return $legacy_token;
		}

		// update a legacy customer ID if it exists.
		if ( 'customer_id' === $key && $order->meta_exists( '_wc_paypal_braintree_customer_id' ) && ! $order->get_meta( $this->get_order_meta_prefix() . $key, true, 'edit' ) && $this->get_id() === $order->get_payment_method( 'edit' ) ) {

			$legacy_customer_id = $order->get_meta( '_wc_paypal_braintree_customer_id', true, 'edit' );

			$order->update_meta_data( $this->get_order_meta_prefix() . $key, $legacy_customer_id );
			$order->delete_meta_data( '_wc_paypal_braintree_customer_id' );
			$order->save_meta_data();

			return $legacy_customer_id;
		}

		return parent::get_order_meta( $order, $key );
	}


	/**
	 * Returns the customer ID for the given user ID. Braintree provides a customer
	 * ID after creation.
	 *
	 * This is overridden to account for merchants that switched to v1 from the
	 * SkyVerge plugin, then updated old subscriptions and/or processed new
	 * subscriptions while waiting for v2.
	 *
	 * @since 2.0.1
	 * @see SV_WC_Payment_Gateway::get_customer_id()
	 * @param int   $user_id WP user ID.
	 * @param array $args optional additional arguments which can include: environment_id, autocreate (true/false), and order.
	 * @return string payment gateway customer id
	 */
	public function get_customer_id( $user_id, $args = array() ) {

		$defaults = array(
			'environment_id' => $this->get_environment(),
			'autocreate'     => false,
			'order'          => null,
		);

		$args = array_merge( $defaults, $args );

		$customer_ids = get_user_meta( $user_id, $this->get_customer_id_user_meta_name( $args['environment_id'] ) );

		// if there is more than one customer ID, grab the latest and use it.
		if ( is_array( $customer_ids ) && count( $customer_ids ) > 1 ) {

			$customer_id = end( $customer_ids );

			if ( $customer_id ) {

				$this->remove_customer_id( $user_id, $args['environment_id'] );

				$this->update_customer_id( $user_id, $customer_id, $args['environment_id'] );
			}
		}

		return parent::get_customer_id( $user_id, $args );
	}


	/**
	 * Ensure a customer ID is created in Braintree for guest customers
	 *
	 * A customer ID must exist in Braintree before it can be used so a guest
	 * customer ID cannot be generated on the fly. This ensures a customer is
	 * created when a payment method is tokenized for transactions such as a
	 * pre-order guest purchase.
	 *
	 * @since 3.1.1
	 * @see SV_WC_Payment_Gateway::get_guest_customer_id()
	 * @param WC_Order $order The order object.
	 * @return bool false
	 */
	public function get_guest_customer_id( \WC_Order $order ) {

		// is there a customer id already tied to this order?
		if ( $customer_id = $this->get_order_meta( $order, 'customer_id' ) ) {
			return $customer_id;
		}

		// default to false as a customer must be created first.
		return false;
	}


	/**
	 * Returns the merchant account transaction URL for the given order
	 *
	 * @since 3.0.0
	 * @see WC_Payment_Gateway::get_transaction_url()
	 * @param \WC_Order $order the order object.
	 * @return string transaction URL
	 */
	public function get_transaction_url( $order ) {

		$merchant_id    = $this->get_merchant_id();
		$transaction_id = $this->get_order_meta( $order, 'trans_id' );
		$environment    = $this->get_order_meta( $order, 'environment' );

		if ( $merchant_id && $transaction_id ) {

			$this->view_transaction_url = sprintf(
				'https://%s.braintreegateway.com/merchants/%s/transactions/%s',
				$this->is_test_environment( $environment ) ? 'sandbox' : 'www',
				$merchant_id,
				$transaction_id
			);
		}

		return parent::get_transaction_url( $order );
	}


	/**
	 * Returns true if the gateway is properly configured to perform transactions
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway::is_configured()
	 * @return boolean true if the gateway is properly configured
	 */
	public function is_configured() {

		$is_configured = parent::is_configured();

		if ( $this->is_connected() && ! $this->is_connected_manually() ) {
			$is_configured = true;
		} elseif ( ! $this->get_merchant_id() || ! $this->get_public_key() || ! $this->get_private_key() ) {
			$is_configured = false;
		}

		return $is_configured;
	}


	/**
	 * Determines if the gateway is connected via Braintree Auth.
	 *
	 * @since 2.0.0
	 * @return bool
	 */
	public function is_connected() {

		$token = $this->get_auth_access_token();

		return ! empty( $token );
	}


	/**
	 * Determines if the merchant can use Braintree Auth.
	 *
	 * Right now this checks that the shop is US-based and transacting in USD.
	 *
	 * @since 2.0.0
	 * @return bool
	 */
	public function can_connect() {

		return 'US' === WC()->countries->get_base_country() && 'USD' === get_woocommerce_currency();
	}


	/**
	 * Determines if the API is connected via standard credentials.
	 *
	 * @since 2.0.0
	 * @return bool
	 */
	public function is_connected_manually() {

		return 'yes' === $this->connect_manually || ! $this->can_connect();
	}


	/**
	 * Returns true if the current page contains a payment form
	 *
	 * @since 3.0.0
	 * @return bool
	 */
	public function is_payment_form_page() {

		return ( ( is_checkout() || has_block( 'woocommerce/checkout' ) ) && ! is_order_received_page() ) || is_checkout_pay_page() || is_add_payment_method_page();
	}


	/**
	 * Get the API object
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway::get_api()
	 * @return \WC_Braintree_API instance
	 */
	public function get_api() {

		if ( is_object( $this->api ) ) {
			return $this->api;
		}

		return $this->api = new WC_Braintree_API( $this );
	}

	/**
	 * Get the SDK object
	 *
	 * @since 3.4.0
	 *
	 * @return \Braintree\Gateway instance
	 */
	public function get_sdk() {

		if ( is_object( $this->sdk ) ) {
			return $this->sdk;
		}

		$this->sdk = new Braintree\Gateway(
			[
				'environment' => $this->get_environment(),
				'merchantId'  => $this->get_merchant_id(),
				'publicKey'   => $this->get_public_key(),
				'privateKey'  => $this->get_private_key(),
			]
		);

		return $this->sdk;
	}

	/**
	 * Returns true if the current gateway environment is configured to 'sandbox'
	 *
	 * @since 3.0.0
	 * @see SV_WC_Payment_Gateway::is_test_environment()
	 * @param string $environment_id optional environment id to check, otherwise defaults to the gateway current environment.
	 * @return boolean true if $environment_id (if non-null) or otherwise the current environment is test
	 */
	public function is_test_environment( $environment_id = null ) {

		// if an environment is passed in, check that.
		if ( ! is_null( $environment_id ) ) {
			return self::ENVIRONMENT_SANDBOX === $environment_id;
		}

		// otherwise default to checking the current environment.
		return $this->is_environment( self::ENVIRONMENT_SANDBOX );
	}


	/**
	 * Gets configured environment.
	 *
	 * If connected to Braintree Auth, the environment was explicitly set at
	 * the time of authentication. Otherwise, use the standard setting.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_environment() {

		if ( $this->is_connected() && ! $this->is_connected_manually() ) {
			$environment = $this->get_auth_environment();
		} else {
			$environment = parent::get_environment();
		}

		return $environment;
	}

	/**
	 * Get the remote configuration for this gateway (lazy loaded).
	 *
	 * @since 3.7.0
	 * @return \WC_Braintree\WC_Braintree_Remote_Configuration
	 */
	protected function get_remote_config() {
		if ( null === $this->remote_config ) {
			// If the credentials are manual, use the current gateway ID.
			// Otherwise, use the credentials source gateway ID.
			$this->remote_config = WC_Braintree_Remote_Configuration::get_remote_configuration( $this->get_credentials_source() );
		}

		return $this->remote_config;
	}

	/**
	 * Returns the source gateway ID for the gateway credentials.
	 * If the source gateway is manual, return the current gateway ID.
	 * Otherwise, return the source gateway ID.
	 *
	 * @since 3.7.0
	 * @return string
	 */
	public function get_credentials_source() {
		$source_gateway_id = $this->get_option( 'inherit_settings_source', 'manual' );
		if ( 'manual' === $source_gateway_id ) {
			return $this->get_id();
		}

		return $source_gateway_id;
	}


	/**
	 * Returns true if the gateway is PayPal
	 *
	 * @since 3.2.0
	 * @return bool
	 */
	public function is_paypal_gateway() {

		return WC_Gateway_Braintree_PayPal::PAYMENT_TYPE_PAYPAL === $this->get_payment_type();
	}


	/**
	 * Returns true if the gateway is Venmo
	 *
	 * @since 3.6.0
	 * @return bool
	 */
	public function is_venmo_gateway() {

		return WC_Gateway_Braintree_Venmo::PAYMENT_TYPE_VENMO === $this->get_payment_type();
	}


	/**
	 * Determines if this is a gateway that supports charging virtual-only orders.
	 *
	 * @since 2.0.0
	 * @return bool
	 */
	public function supports_credit_card_charge_virtual() {
		return $this->supports( self::FEATURE_CREDIT_CARD_CHARGE_VIRTUAL );
	}


	/**
	 * Returns the merchant ID based on the current environment
	 *
	 * @since 3.0.0
	 * @param string $environment_id optional one of 'sandbox' or 'production', defaults to current configured environment.
	 * @return string merchant ID
	 */
	public function get_merchant_id( $environment_id = null ) {

		if ( $this->is_connected() && ! $this->is_connected_manually() ) {
			return $this->get_auth_merchant_id();
		}

		if ( is_null( $environment_id ) ) {
			$environment_id = $this->get_environment();
		}

		return self::ENVIRONMENT_PRODUCTION === $environment_id ? $this->merchant_id : $this->sandbox_merchant_id;
	}


	/**
	 * Gets the Braintree Auth access token.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_auth_access_token() {

		return $this->auth_access_token;
	}


	/**
	 * Gets the Braintree Auth merchant ID.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_auth_environment() {

		return get_option( 'wc_braintree_auth_environment', self::ENVIRONMENT_PRODUCTION );
	}


	/**
	 * Gets the Braintree Auth merchant ID.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_auth_merchant_id() {

		return get_option( 'wc_braintree_auth_merchant_id', '' );
	}


	/**
	 * Returns the public key based on the current environment
	 *
	 * @since 3.0.0
	 * @param string $environment_id optional one of 'sandbox' or 'production', defaults to current configured environment.
	 * @return string public key
	 */
	public function get_public_key( $environment_id = null ) {

		if ( is_null( $environment_id ) ) {
			$environment_id = $this->get_environment();
		}

		return self::ENVIRONMENT_PRODUCTION === $environment_id ? $this->public_key : $this->sandbox_public_key;
	}


	/**
	 * Returns the private key based on the current environment
	 *
	 * @since 3.0.0
	 * @param string $environment_id optional one of 'sandbox' or 'production', defaults to current configured environment.
	 * @return string private key
	 */
	public function get_private_key( $environment_id = null ) {

		if ( is_null( $environment_id ) ) {
			$environment_id = $this->get_environment();
		}

		return self::ENVIRONMENT_PRODUCTION === $environment_id ? $this->private_key : $this->sandbox_private_key;
	}


	/**
	 * Return the merchant account ID for the given currency and environment
	 *
	 * @since 3.0.0
	 * @param string|null $currency optional currency code, defaults to base WC currency.
	 * @return string|null
	 */
	public function get_merchant_account_id( $currency = null ) {

		if ( is_null( $currency ) ) {
			$currency = get_woocommerce_currency();
		}

		$key = 'merchant_account_id_' . strtolower( $currency );

		return isset( $this->$key ) ? $this->$key : null;
	}


	/**
	 * Return an array of valid Braintree environments
	 *
	 * @since 3.0.0
	 * @return array
	 */
	protected function get_braintree_environments() {

		return array(
			self::ENVIRONMENT_PRODUCTION => __( 'Production', 'woocommerce-gateway-paypal-powered-by-braintree' ),
			self::ENVIRONMENT_SANDBOX    => __( 'Sandbox', 'woocommerce-gateway-paypal-powered-by-braintree' ),
		);
	}


	/**
	 * Determines if a dynamic descriptor name value is valid.
	 *
	 * @since 2.1.0
	 *
	 * @param string $value name to check. Defaults to the gateway's configured setting.
	 * @return bool
	 */
	public function is_name_dynamic_descriptor_valid( $value = '' ) {

		if ( ! $value ) {
			$value = $this->get_name_dynamic_descriptor();
		}

		// missing asterisk.
		if ( false === strpos( $value, '*' ) ) {
			return false;
		}

		$parts = explode( '*', $value );

		$company = $parts[0];
		$product = $parts[1];

		switch ( strlen( $company ) ) {

			case 3:
				$product_length = 18;
				break;
			case 7:
				$product_length = 14;
				break;
			case 12:
				$product_length = 9;
				break;

			// if any other length, bail.
			default:
				return false;
		}

		if ( strlen( $product ) > $product_length ) {
			return false;
		}

		return true;
	}


	/**
	 * Return the name dynamic descriptor
	 *
	 * @link https://developers.braintreepayments.com/reference/request/transaction/sale/php#descriptor.name
	 * @since 3.0.0
	 * @return string
	 */
	public function get_name_dynamic_descriptor() {

		return $this->name_dynamic_descriptor;
	}


	/**
	 * Determines if a phone dynamic descriptor value is valid.
	 *
	 * The value must be 14 characters or less, have exactly 10 digits, and
	 * otherwise contain only numbers, dashes, parentheses, or periods.
	 *
	 * @since 2.1.0
	 *
	 * @param string $value value to check. Defaults to the gateway's configured setting
	 * @return bool
	 */
	public function is_phone_dynamic_descriptor_valid( $value = '' ) {

		if ( ! $value ) {
			$value = $this->get_phone_dynamic_descriptor();
		}

		// max 14 total characters.
		if ( strlen( $value ) > 14 ) {
			return false;
		}

		// check for invalid characters.
		if ( $invalid_characters = preg_replace( '/[\d\-().]/', '', $value ) ) {
			return false;
		}

		// must have exactly 10 numbers.
		if ( strlen( preg_replace( '/[^0-9]/', '', $value ) ) !== 10 ) {
			return false;
		}

		return true;
	}


	/**
	 * Return the phone dynamic descriptor
	 *
	 * @link https://developers.braintreepayments.com/reference/request/transaction/sale/php#descriptor.phone
	 * @since 3.0.0
	 * @return string
	 */
	public function get_phone_dynamic_descriptor() {
		return $this->phone_dynamic_descriptor;
	}


	/**
	 * Return the URL dynamic descriptor
	 *
	 * @link https://developers.braintreepayments.com/reference/request/transaction/sale/php#descriptor.url
	 * @since 3.0.0
	 * @return string
	 */
	public function get_url_dynamic_descriptor() {
		return $this->url_dynamic_descriptor;
	}


	/**
	 * Gets the transaction type for the gateway.
	 *
	 * @since 2.5.0
	 *
	 * @return string
	 */
	public function get_transaction_type() {

		return $this->get_option( 'transaction_type' );
	}

	/**
	 * Adds the standard transaction data to the order.
	 * This function is added to set transaction ID to order using WooCommerce Order API.
	 *
	 * @since 2.9.1
	 *
	 * @param \WC_Order                               $order    the order object.
	 * @param SV_WC_Payment_Gateway_API_Response|null $response optional transaction response.
	 */
	public function add_transaction_data( $order, $response = null ) {
		if ( $response && $response->get_transaction_id() ) {
			// Save transaction id if available.
			if ( is_numeric( $order ) ) {
				$order = wc_get_order( $order );
			}

			if ( $order instanceof \WC_Order ) {
				$order->set_transaction_id( $response->get_transaction_id() );
				$order->save();
			}
		}

		return parent::add_transaction_data( $order, $response );
	}

	/**
	 * Handles payment processing.
	 *
	 * @see WC_Payment_Gateway::process_payment()
	 *
	 * @since 3.0.0
	 *
	 * @param int|string $order_id Order ID.
	 * @return array associative array with members 'result' and 'redirect'
	 */
	public function process_payment( $order_id ) {
		/**
		 * Direct Gateway Process Payment Filter.
		 *
		 * Allow actors to intercept and implement the process_payment() call for
		 * this transaction. Return an array value from this filter will return it
		 * directly to the checkout processing code and skip this method entirely.
		 *
		 * @since 3.2.0
		 *
		 * @param bool $result default true
		 * @param int|string $order_id order ID for the payment
		 * @param SV_WC_Payment_Gateway_Direct $this instance
		 */
		$result = apply_filters( 'wc_payment_gateway_' . $this->get_id() . '_process_payment', true, $order_id, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		// add payment information to order.
		$order = $this->get_order( $order_id );

		try {

			// handle creating or updating a payment method for registered customers if tokenization is enabled.
			if ( $this->supports_tokenization() && 0 !== (int) $order->get_user_id() ) {

				// if already paying with an existing method, try and updated it locally and remotely.
				if ( ! empty( OrderHelper::get_property( $order, 'payment', 'token' ) ) ) {

					$this->update_transaction_payment_method( $order );

					// otherwise, create a new token if desired.
				} elseif ( $this->should_tokenize_before_sale( $order ) ) {

					$order = $this->get_payment_tokens_handler()->create_token( $order );
				}
			}

			// payment failures are handled internally by do_transaction()
			// note that customer id & payment token are saved to order when create_token() is called.
			if ( $this->should_skip_transaction( $order ) || $this->do_transaction( $order ) ) {

				// This meta is used to prevent 3DS verification.
				if ( $this->should_tokenize_apple_pay_card() ) {
					$stored_tokens = \WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), \WC_Braintree\WC_Braintree::CREDIT_CARD_GATEWAY_ID );

					foreach ( $stored_tokens as $stored_token_id => $stored_token_object ) {
						if ( $stored_token_object->get_token() === OrderHelper::get_property( $order, 'payment', 'token' ) ) {
							$stored_token_object->add_meta_data( 'instrument_type', 'apple_pay' );
							$stored_token_object->save();
						}
					}
				}
				if ( $this->should_tokenize_google_pay_card() ) {
					$stored_tokens = \WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), \WC_Braintree\WC_Braintree::CREDIT_CARD_GATEWAY_ID );

					foreach ( $stored_tokens as $stored_token_id => $stored_token_object ) {
						if ( $stored_token_object->get_token() === OrderHelper::get_property( $order, 'payment', 'token' ) ) {
							$stored_token_object->add_meta_data( 'instrument_type', 'google_pay' );
							$stored_token_object->save();
						}
					}
				}

				// add transaction data for zero-dollar "orders".
				if ( '0.00' === OrderHelper::get_payment_total( $order ) ) {
					$this->add_transaction_data( $order );
				}

				/**
				 * Filters the order status that's considered to be "held".
				 *
				 * @since 3.2.0
				 *
				 * @param string $status held order status.
				 * @param \WC_Order $order order object.
				 * @param SV_WC_Payment_Gateway_API_Response|null $response API response object, if any.
				 */
				$held_order_status = apply_filters( 'wc_' . $this->get_id() . '_held_order_status', 'on-hold', $order, null );

				if ( $order->has_status( $held_order_status ) ) {
					// reduce stock for held orders, but don't complete payment (pass order ID so WooCommerce fetches fresh order object with reduced_stock meta set on order status change).
					wc_reduce_stock_levels( $order->get_id() );
				} else {
					// mark order as having received payment.
					$order->payment_complete();
				}

				// process_payment() can sometimes be called in an admin-context.
				if ( isset( WC()->cart ) ) {
					WC()->cart->empty_cart();
				}

				/**
				 * Payment Gateway Payment Processed Action.
				 *
				 * Fired when a payment is processed for an order.
				 *
				 * @since 3.2.0
				 *
				 * @param \WC_Order $order order object.
				 * @param SV_WC_Payment_Gateway_Direct $this instance.
				 */
				do_action( 'wc_payment_gateway_' . $this->get_id() . '_payment_processed', $order, $this );

				$result = array(
					'result'   => 'success',
					'redirect' => $this->get_return_url( $order ),
				);

				$messages = array();

				/*
				 * Only get user messages if the session is available.
				 *
				 * The get_notices_as_user_messages() method makes use of the wc_get_notices()
				 * function which assumes the presence of the WC session. This code may be called
				 * in an admin context where the session is not available.
				 *
				 * See https://github.com/woocommerce/woocommerce/issues/48023
				 * See https://github.com/woocommerce/woocommerce-gateway-paypal-powered-by-braintree/issues/614
				 */
				if ( isset( WC()->session ) ) {
					$messages = $this->get_notices_as_user_messages();
				}

				if ( $this->debug_checkout() && $messages ) {
					$result['message'] = ! empty( $messages ) ? implode( "\n", $messages ) : '';
				}
			} else {

				$messages = array();

				/*
				 * Only get user messages if the session is available.
				 *
				 * The get_notices_as_user_messages() method makes use of the wc_get_notices()
				 * function which assumes the presence of the WC session. This code may be called
				 * in an admin context where the session is not available.
				 *
				 * See https://github.com/woocommerce/woocommerce/issues/48023
				 * See https://github.com/woocommerce/woocommerce-gateway-paypal-powered-by-braintree/issues/614
				 */
				if ( isset( WC()->session ) ) {
					$messages = $this->get_notices_as_user_messages();
				}

				$result = array(
					'result'  => 'failure',
					'message' => ! empty( $messages ) ? implode( "\n", $messages ) : __( 'The transaction failed.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				);
			}
		} catch ( Framework\SV_WC_Plugin_Exception $exception ) {

			$this->mark_order_as_failed( $order, $exception->getMessage() );

			$result = array(
				'result'  => 'failure',
				'message' => $exception->getMessage(),
			);
		}

		// If the payment failed, add the error messages to the result.
		if ( 'failure' === $result['result'] && function_exists( 'wc_get_notices' ) ) {
			$notices = wc_get_notices( 'error' );
			if ( ! empty( $notices ) ) {
				$messages = array();
				foreach ( $notices as $notice ) {
					$messages[] = isset( $notice['notice'] ) ? $notice['notice'] : $notice;
				}

				if ( ! empty( $messages ) ) {
					$result['message'] = implode( '. ', $messages );
				}
			}
		}

		// Replace error message for status code 91564 (Cannot use a paymentMethodNonce more than once).
		if ( isset( $result['message'] ) && false !== strpos( $result['message'], 'Status code 91564:' ) ) {
			wc_clear_notices();
			// Add custom user-friendly notice.
			wc_add_notice(
				esc_html__( 'An error occurred while processing your payment, please reload the page and try again, or try an alternate payment method.', 'woocommerce-gateway-paypal-powered-by-braintree' ),
				'error'
			);
			// No need to update the result, we just want to replace the customer facing message.
		}

		return $result;
	}

	/**
	 * Mark an order as refunded. This should only be used when the full order
	 * amount has been refunded.
	 *
	 * @since 3.1.5
	 *
	 * @param \WC_Order $order order object.
	 */
	public function mark_order_as_refunded( $order ) {

		/* translators: Placeholders: %s - payment gateway title (such as Authorize.net, Braintree, etc) */
		$order_note = sprintf( esc_html__( '%s Order completely refunded.', 'woocommerce-gateway-paypal-powered-by-braintree' ), $this->get_method_title() );

		// Add order note and continue with WC refund process.
		$order->add_order_note( $order_note );
	}

	/**
	 * Check if the gateway has an account connected.
	 *
	 * @since 3.2.6
	 *
	 * @return bool True if the gateway has an account connected, false otherwise.
	 */
	public function is_account_connected() {
		return $this->is_configured();
	}

	/**
	 * Checks if the cart contains a subscription product.
	 *
	 * @since 3.7.0
	 *
	 * @return bool
	 */
	public function cart_contains_subscription() {

		if ( ! $this->get_plugin()->is_subscriptions_active() || ! class_exists( 'WC_Subscriptions_Cart' ) || ! method_exists( 'WC_Subscriptions_Cart', 'cart_contains_subscription' ) ) {
			return false;
		}

		return \WC_Subscriptions_Cart::cart_contains_subscription();
	}

	/**
	 * Returns true if the current gateway environment is configured to 'sandbox'
	 *
	 * @since 3.2.6
	 *
	 * @return boolean true if the current environment is test environment.
	 */
	public function is_in_test_mode() {
		return $this->is_test_environment();
	}

	/**
	 * Determine if the gateway still requires setup.
	 *
	 * @return bool
	 */
	public function needs_setup() {
		return ! $this->is_configured();
	}


	/** Webhook Methods *******************************************************/

	/**
	 * Get the webhook URL for the current environment
	 *
	 * @since 3.5.0
	 * @return string
	 */
	public function get_webhook_url() {
		return add_query_arg(
			[
				'wc-api' => 'wc_braintree',
			],
			home_url( '/' )
		);
	}
}
