skyverge/wc-plugin-framework

View on GitHub
woocommerce/payment-gateway/Blocks/Gateway_Checkout_Block_Integration.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
/**
 * WooCommerce Payment Gateway Framework
 *
 * 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@skyverge.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade the plugin to newer
 * versions in the future. If you wish to customize the plugin for your
 * needs please refer to http://www.skyverge.com
 *
 * @package   SkyVerge/WooCommerce/Payment-Gateway/External_Checkout
 * @author    SkyVerge
 * @copyright Copyright (c) 2013-2024, SkyVerge, Inc.
 * @license   http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
 */

namespace SkyVerge\WooCommerce\PluginFramework\v5_12_4\Payment_Gateway\Blocks;

use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use SkyVerge\WooCommerce\PluginFramework\v5_12_4\SV_WC_Payment_Gateway;
use SkyVerge\WooCommerce\PluginFramework\v5_12_4\SV_WC_Payment_Gateway_Helper;
use SkyVerge\WooCommerce\PluginFramework\v5_12_4\SV_WC_Payment_Gateway_Payment_Form;
use SkyVerge\WooCommerce\PluginFramework\v5_12_4\SV_WC_Payment_Gateway_Payment_Token;
use SkyVerge\WooCommerce\PluginFramework\v5_12_4\SV_WC_Payment_Gateway_Plugin;
use SkyVerge\WooCommerce\PluginFramework\v5_12_4\Blocks\Traits\Block_Integration_Trait;
use WC_HTTPS;
use WC_Subscriptions_Cart;

if ( ! class_exists( '\SkyVerge\WooCommerce\PluginFramework\v5_12_4\Payment_Gateway\Blocks\Gateway_Checkout_Block_Integration' ) ) :

/**
 * Base class for handling support for the WooCommerce Checkout block in gateways.
 *
 * For support in non-gateways, {@see Block_Integration}.
 *
 * @since 5.12.0
 */
#[\AllowDynamicProperties]
abstract class Gateway_Checkout_Block_Integration extends AbstractPaymentMethodType {


    use Block_Integration_Trait;


    /** @var SV_WC_Payment_Gateway_Plugin instance of the current plugin */
    protected SV_WC_Payment_Gateway_Plugin $plugin;

    /** @var SV_WC_Payment_Gateway gateway handling integration */
    protected SV_WC_Payment_Gateway $gateway;

    /** @var string supported block name */
    protected string $block_name = 'checkout';


    /**
     * Block integration constructor.
     *
     * @since 5.12.0
     *
     * @param SV_WC_Payment_Gateway_Plugin $plugin
     * @param SV_WC_Payment_Gateway $gateway
     */
    public function __construct( SV_WC_Payment_Gateway_Plugin $plugin, SV_WC_Payment_Gateway $gateway ) {

        $this->plugin   = $plugin;
        $this->gateway  = $gateway;
        $this->settings = $gateway->settings;

        $this->add_hooks();
    }


    /**
     * Adds hooks.
     *
     * @since 5.12.0
     *
     * @return void
     */
    protected function add_hooks(): void {

        // extends the block payment data for the current gateway
        add_filter( 'wc_' . $this->get_name() . '_' . $this->block_name . '_block_payment_method_data', [ $this, 'add_payment_method_data' ], 10, 2 );

        // prepares payment data for processing in the backend
        add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'prepare_payment_data' ], 10, 2 );

        // AJAX endpoint hooks for front-end logging
        $this->add_ajax_logging();
    }


    /**
     * Determines if the payment method is available in the checkout block context.
     *
     * @since 5.12.0
     *
     * @return bool
     */
    public function is_active() : bool {

        return $this->gateway->is_available();
    }


    /**
     * Determines if AJAX logging is enabled for the gateway.
     *
     * @since 5.12.0
     *
     * @return bool
     */
    protected function is_ajax_logging_enabled(): bool {

        return ! $this->gateway->debug_off();
    }


    /**
     * Gets the payment method script handles.
     *
     * Defaults to {@see get_script_handles()} but concrete implementations may override this.
     *
     * @since 5.12.0
     *
     * @return string[]
     */
    public function get_payment_method_script_handles() : array {

        return $this->get_script_handles();
    }


    /**
     * Gets the payment method data.
     *
     * @since 5.12.0
     *
     * @return array<string, mixed>
     */
    public function get_payment_method_data() : array {

        $payment_method_data = [
            'id'                   => $this->gateway->get_id_dasherized(), // dashes
            'name'                 => $this->gateway->get_id(), // underscores
            'type'                 => $this->gateway->get_payment_type(), // typically 'credit-card' or 'echeck', for internal use
            'title'                => $this->gateway->get_title(), // user-facing display title
            'description'          => $this->gateway->get_description(), // user-facing description
            'icons'                => $this->get_gateway_icons(), // icon or card icons displayed next to title
            'supported_card_types' => $this->get_supported_card_types(), // card types that the gateway supports, regardless if enabled in settings or not
            'enabled_card_types'   => $this->get_enabled_card_types(), // card types that are enabled in settings
            'defaults'             => $this->get_gateway_defaults(), // used to pre-populate payment method fields (typically in test mode)
            'placeholders'         => $this->get_placeholders(), // used in some payment method fields
            'supports'             => array_values( $this->gateway->supports ), // list of supported features
            'flags'                => $this->get_gateway_flags(), // list of gateway configuration flags
            'gateway'              => $this->get_gateway_configuration(), // other gateway configuration values
            'apple_pay'            => $this->get_apple_pay_configuration(), // Apple Pay configuration values
            'google_pay'           => $this->get_google_pay_configuration(), // Google Pay configuration values
            'debug_mode'           => $this->get_debug_mode(), // the current debug mode (log, checkout, full, off)
            'i18n'                 => $this->gateway->get_gateway_payment_form_localized_params(),
            'date_format'          => wc_date_format(),
            'time_format'          => wc_time_format(),
            'sample_check'         => WC_HTTPS::force_https_url( $this->plugin->get_payment_gateway_framework_assets_url(). '/images/sample-check-sprite.png' ),
            'help_tip'             => WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/help.png' ),
            'ajax_url'             => WC_HTTPS::force_https_url( admin_url( 'admin-ajax.php' ) ),
            'ajax_log_nonce'       => wp_create_nonce( 'wc_' . $this->get_name() . '_' . $this->block_name . '_block_log' ),
        ];

        /**
         * Filters gateway-specific payment method data for the Checkout Block.
         *
         * @since 5.12.0
         *
         * @param array<string, mixed> $payment_method_data
         * @param SV_WC_Payment_Gateway $gateway
         */
        return (array) apply_filters( "wc_{$this->gateway->get_id()}_{$this->block_name}_block_payment_method_data", $payment_method_data, $this->gateway );
    }


    /**
     * Gets the payment data as a JSON string.
     *
     * @since 5.12.0
     *
     * @return string JSON
     */
    public function get_payment_method_data_json() : string {

        return wp_json_encode( $this->get_payment_method_data() );
    }


    /**
     * Adds payment method data.
     *
     * Plugins that extend this class may override this method to add additional payment method data.
     * @see Gateway_Checkout_Block_Integration::get_payment_method_data()
     *
     * @since 5.12.0
     *
     * @param array $payment_method_data
     * @param SV_WC_Payment_Gateway $gateway
     * @return array
     */
    public function add_payment_method_data( array $payment_method_data, SV_WC_Payment_Gateway $gateway ) : array {

        return $payment_method_data;
    }


    /**
     * Gets all the supported card types for this gateway.
     *
     * Some gateways don't support all card types.
     *
     * @since 5.12.0
     *
     * @return string[]
     */
    protected function get_supported_card_types() : array {

        if ( ! $this->gateway->supports_card_types() ) {
            return [];
        }

        return array_map( [ SV_WC_Payment_Gateway_Helper::class, 'normalize_card_type' ], array_keys( $this->gateway->get_available_card_types() ) );
    }


    /**
     * Gets all the enabled card types for this gateway.
     *
     * A gateway could support more card types, but the merchant may have disabled some.
     *
     * @since 5.12.0
     *
     * @return string[]
     */
    protected function get_enabled_card_types() : array {

        if ( ! $this->gateway->supports_card_types() ) {
            return [];
        }

        return array_map( [ SV_WC_Payment_Gateway_Helper::class, 'normalize_card_type' ], $this->gateway->get_card_types() );
    }


    /**
     * Gets a list of gateway logos as icon image URLs.
     *
     * If the gateway has a specific icon, it will return that item only.
     * Otherwise, it will return a list of icon URLs for each card type supported by the gateway.
     *
     * @since 5.12.0
     *
     * @return array<string, string>
     */
    protected function get_gateway_icons() : array {

        $icons = [];

        if ( $this->gateway->icon ) {

            $icons = [ $this->gateway->get_method_title() => $this->gateway->icon ];

        } elseif ( $this->gateway->is_echeck_gateway() ) {

            $icons = [ __( 'eCheck', 'woocommerce' ) => $this->gateway->get_payment_method_image_url( 'echeck' ) ];

        } elseif ( $card_types = $this->get_enabled_card_types() ) {

            foreach ( $card_types as $card_type ) {

                $card_type = SV_WC_Payment_Gateway_Helper::normalize_card_type( $card_type );
                $card_name = SV_WC_Payment_Gateway_Helper::payment_type_to_name( $card_type );

                if ( $url = $this->gateway->get_payment_method_image_url( $card_type ) ) {
                    $icons[ $card_name ] = WC_HTTPS::force_https_url( $url );
                }
            }
        }

        /**
         * Filters the payment gateway icons for the Checkout block.
         *
         * If the gateway specifies an icon or is an eCheck type, it will return that item only.
         * If the gateway doesn't, but supports card types, it will return a list of icon URLs for each card type supported by the gateway.
         *
         * @since 5.12.0
         *
         * @param array<string, string> $icons list of icon URLs keyed by payment method or card name
         * @param SV_WC_Payment_Gateway $gateway
         */
        return apply_filters( "wc_{$this->gateway->get_id()}_{$this->block_name}_block_payment_method_icons", $icons, $this->gateway );
    }


    /**
     * Gets the payment method fields placeholder.
     *
     * @since 5.12.0
     *
     * @return array<string, mixed>
     */
    protected function get_placeholders() : array {

        $placeholders = [
            'credit_card_number'  => '•••• •••• •••• ••••',
            'credit_card_expiry'  => 'MM/YY',
            'credit_card_csc'     => '•••',
            'bank_routing_number' => '•••••••••',
        ];

        /**
         * Filters the payment gateway placeholders for the Checkout block.
         *
         * @since 5.12.0
         *
         * @param array<string, mixed> $placeholders
         * @param SV_WC_Payment_Gateway $gateway
         */
        return (array) apply_filters( "wc_{$this->gateway->get_id()}_{$this->block_name}_block_payment_method_placeholders", $placeholders, $this->gateway );
    }


    /**
     * Gets the gateway defaults.
     *
     * @since 5.12.0
     *
     * @return array<string, mixed>
     */
    protected function get_gateway_defaults() : array {

        if ( ! $this->gateway->supports_payment_form() ) {
            return [];
        }

        $defaults = [];

        // this is needed because some keys may use dashes instead of underscores, which could cause trouble when parsed as JS objects
        foreach ( $this->gateway->get_payment_method_defaults() as $default_key => $default_value ) {
            $defaults[ str_replace( '-', '_', $default_key ) ] = $default_value;
        }

        return $defaults;
    }


    /**
     * Returns any gateway flags from configuration (boolean values only).
     *
     * @since 5.12.0
     *
     * @return array<string, bool>
     */
    protected function get_gateway_flags() : array {

        $flags = [
            'settings_inherited'                => $this->gateway->inherit_settings(),
            'is_test_environment'               => $this->gateway->is_test_environment(),
            'is_credit_card_gateway'            => $this->gateway->is_credit_card_gateway(),
            'is_echeck_gateway'                 => $this->gateway->is_echeck_gateway(),
            'csc_enabled'                       => $this->gateway->csc_enabled(),
            'csc_enabled_for_tokens'            => $this->gateway->csc_enabled_for_tokens(),
            'tokenization_enabled'              => $this->gateway->supports_tokenization() && $this->gateway->tokenization_enabled(),
            'detailed_decline_messages_enabled' => $this->gateway->is_detailed_customer_decline_messages_enabled(),
            'logging_enabled'                   => 'off' !== $this->get_debug_mode(),
            'has_subscription'                  => false,
            'has_subscription_renewal'          => false,
        ];

        $force_tokenization = false;

        if ( $this->plugin->is_subscriptions_active() && class_exists( 'WC_Subscriptions_Cart' ) ) {

            $cart_has_subscription = $force_tokenization = WC_Subscriptions_Cart::cart_contains_subscription();

            $flags['has_subscription']         = $cart_has_subscription;
            $flags['has_subscription_renewal'] = $cart_has_subscription && isset( WC()->cart->recurring_carts ) && ! empty( WC()->cart->recurring_carts );
        }

        $flags['tokenization_forced'] = $force_tokenization;

        return $flags;
    }


    /**
     * Gets any gateway configuration values.
     *
     * @since 5.12.0
     *
     * @return array<string, mixed>
     */
    protected function get_gateway_configuration() : array {

        return [
            'transaction_type'       => $this->gateway->supports_credit_card_authorization() & $this->gateway->supports_credit_card_charge() ? $this->gateway->get_option( 'transaction_type', 'charge' ) : null,
            'charge_virtual_orders'  => $this->gateway->supports_credit_card_charge_virtual() ? 'yes' === $this->gateway->get_option( 'charge_virtual', 'no' ) : null,
            'enable_partial_capture' => $this->gateway->supports_credit_card_partial_capture() ? 'yes' === $this->gateway->get_option( 'enable_partial_capture', 'no' ) : null,
            'enable_paid_capture'    => $this->gateway->supports_credit_card_capture() ? 'yes' === $this->gateway->get_option( 'enable_paid_capture', 'no' ) : null,
        ];
    }


    /**
     * Gets the Apple Pay configuration for the current gateway, if available.
     *
     * @since 5.12.0
     *
     * @return array<string, mixed>|null returns null if Apple Pay is unsupported by the gateway
     */
    protected function get_apple_pay_configuration() : ?array {

        $apple_pay_configuration = null;

        if ( $this->gateway->supports_apple_pay() ) {

            $apple_pay          = $this->plugin->get_apple_pay_instance();
            $processing_gateway = $apple_pay ? $apple_pay->get_processing_gateway() : null;

            if ( $processing_gateway && $this->gateway->get_id() === $processing_gateway->get_id() ) {

                $apple_pay_configuration = [
                    'merchant_id'              => $apple_pay->get_merchant_id(),
                    'merchant_name'            => get_bloginfo( 'name' ),
                    'validate_nonce'           => wp_create_nonce( 'wc_' . $this->gateway->get_id() . '_apple_pay_validate_merchant' ),
                    'recalculate_totals_nonce' => wp_create_nonce( 'wc_' . $this->gateway->get_id() . '_apple_pay_recalculate_totals' ),
                    'process_nonce'            => wp_create_nonce( 'wc_' . $this->gateway->get_id() . '_apple_pay_process_payment' ),
                    'button_style'             => $apple_pay->get_button_style(),
                    'card_types'               => $apple_pay->get_supported_networks(),
                    'countries'                => $this->gateway->get_available_countries(),
                    'currencies'               => $this->gateway->get_apple_pay_currencies(),
                    'capabilities'             => $this->gateway->get_apple_pay_capabilities(),
                    'flags'                    => [
                        'is_enabled'          => $apple_pay->is_enabled(),
                        'is_available'        => $apple_pay->is_available() && $apple_pay->supports_checkout_block(),
                        'is_test_environment' => $apple_pay->is_test_mode(),
                    ],
                ];
            }
        }

        return $apple_pay_configuration;
    }


    /**
     * Gets the Google Pay configuration for the current gateway, if available.
     *
     * @since 5.12.0
     *
     * @return array<string, mixed>|null returns null if Google Pay is unsupported by the gateway
     */
    protected function get_google_pay_configuration() : ?array {

        $google_pay_configuration = null;

        if ( $this->gateway->supports_google_pay() ) {

            $google_pay         = $this->plugin->get_google_pay_instance();
            $processing_gateway = $google_pay ? $google_pay->get_processing_gateway() : null;

            if ( $processing_gateway && $this->gateway->get_id() === $processing_gateway->get_id() ) {

                $google_pay_configuration = [
                    'merchant_id'              => $google_pay->get_merchant_id(),
                    'merchant_name'            => get_bloginfo( 'name' ),
                    'recalculate_totals_nonce' => wp_create_nonce( 'wc_' . $this->gateway->get_id() . '_google_pay_recalculate_totals' ),
                    'process_nonce'            => wp_create_nonce( 'wc_' . $this->gateway->get_id() . '_google_pay_process_payment' ),
                    'button_style'             => $google_pay->get_button_style(),
                    'card_types'               => $google_pay->get_supported_networks(),
                    'countries'                => $google_pay->get_available_countries(),
                    'currencies'               => [ get_woocommerce_currency() ],
                    'flags'                    => [
                        'is_enabled'          => $google_pay->is_enabled(),
                        'is_available'        => $google_pay->is_available() && $google_pay->supports_checkout_block(),
                        'is_test_environment' => $google_pay->is_test_mode(),
                    ],
                ];
            }
        }

        return $google_pay_configuration;
    }


    /**
     * Gets the debug mode configured for the gateway.
     *
     * - `off`: no logging
     * - `log`: save to log file
     * - `checkout`: save to log file and display in checkout page
     * - `full`: save to log file and display in checkout page
     *
     * @since 5.12.0
     *
     * @return string one of off, log, checkout or full
     */
    protected function get_debug_mode() : string {

        $debug_mode = $this->gateway->get_debug_mode();

        return 'both' === $debug_mode ? 'full' : $debug_mode;
    }


    /**
     * Prepare payment data for processing by the gateway.
     *
     * This method does not actually process the payment - it simply adjusts the payment data to support legacy processing.
     *
     * The checkout block has built-in support for tokenization & payment tokens, but it sends the data from the frontend
     * with field names and values that our existing payment processing does not expect.
     *
     * For example, it sends the internal (core) token ID instead of the gateway-specific token ID. This method fetches
     * the token based on core ID and injects the gateway-specific token ID into the `PaymentContext::$payment_data`
     * array so that the gateway can process the payment.
     *
     * @see PaymentContext::$payment_data is converted to `$_POST` by WC core when handling legacy payments.
     * @see \Automattic\WooCommerce\StoreApi\Legacy::process_legacy_payment()
     * @see SV_WC_Payment_Gateway::get_processing_context()
     *
     * @internal
     *
     * @since 5.12.0
     *
     * @param PaymentContext $payment_context
     * @param PaymentResult $payment_result
     * @return PaymentResult
     */
    public function prepare_payment_data( PaymentContext $payment_context, PaymentResult $payment_result ) : PaymentResult {

        /**
         * This is a flag to indicate we are processing a block-based checkout, helpful in some gateways that need different handling
         * between blocks checkout and legacy shortcode checkout - this will be accessed in $_POST data along with the other field data.
         * @see SV_WC_Payment_Gateway_Payment_Form::get_payment_fields()
         */
        $additional_payment_data[ 'wc-' . $this->gateway->get_id_dasherized() . '-context' ] = $this->gateway::PROCESSING_CONTEXT_BLOCK;

        /**
         * Fetch the provider-based token ID for the core token ID:
         * @see SV_WC_Payment_Gateway_Direct::get_order()
         */
        if ( $token = $this->get_payment_token_for_context( $payment_context ) ) {
            $additional_payment_data[ 'wc-' . $this->gateway->get_id_dasherized() . '-payment-token' ] = $token->get_id();
        }

        /**
         * Convert the tokenization flag to the expected key-value pair:
         * @see SV_WC_Payment_Gateway_Payment_Tokens_Handler::should_tokenize()
         */
        if ( $should_tokenize = $payment_context->payment_data['wc-' . $this->gateway->get_id() . '-new-payment-method'] ) {
            $additional_payment_data[ 'wc-' . $this->gateway->get_id_dasherized() . '-tokenize-payment-method' ] = $should_tokenize;
        }

        /**
         * Taking advantage of the fact that objects are passed 'by reference' (actually handles) in PHP:
         * @link https://dev.to/nicolus/are-php-objects-passed-by-reference--2gp3
         */
        $payment_context->set_payment_data(
            array_merge(
                $payment_context->payment_data,
                $additional_payment_data
            )
        );

        // return the original payment result
        return $payment_result;
    }


    /**
     * Gets a payment token for a given payment context.
     *
     * @since 5.12.0
     *
     * @param PaymentContext $payment_context
     * @return SV_WC_Payment_Gateway_Payment_Token|null
     */
    protected function get_payment_token_for_context( PaymentContext $payment_context ): ?SV_WC_Payment_Gateway_Payment_Token {

        $core_token_id = $payment_context->payment_data['token'] ?: null;

        if ( ! $core_token_id || $payment_context->payment_method !== $this->gateway->get_id() ) {
            return null;
        }

        return $this->gateway->get_payment_tokens_handler()->get_token_by_core_id( get_current_user_id(), $core_token_id );
    }


}

endif;