diff --git a/.distignore b/.distignore index b7530d3..5084c09 100644 --- a/.distignore +++ b/.distignore @@ -14,6 +14,7 @@ commitlint.config.js composer.json composer-lock.json +composer.lock Dockerfile Dockerfile-* docker-compose.phpunit.yml @@ -32,6 +33,7 @@ readme.md README.md readme.dev.txt README.md +release.config.js renovate.json webpack.config.js wpcron.txt \ No newline at end of file diff --git a/classes/class-ledyer-api.php b/classes/class-ledyer-api.php index c469557..faee266 100644 --- a/classes/class-ledyer-api.php +++ b/classes/class-ledyer-api.php @@ -83,7 +83,12 @@ public function update_order_reference( $order_id, $data ) { * @return mixed|\WP_Error */ public function acknowledge_order( $order_id ) { - return ( new \Ledyer\Requests\Order\Management\Acknowledge_Order( array( 'orderId' => $order_id, 'data' => array() ) ) )->request(); + return ( new \Ledyer\Requests\Order\Management\Acknowledge_Order( + array( + 'orderId' => $order_id, + 'data' => array(), + ) + ) )->request(); } /** @@ -94,4 +99,4 @@ public function acknowledge_order( $order_id ) { public function get_payment_status( $order_id ) { return ( new \Ledyer\Requests\Order\Management\Get_Payment_Status( array( 'orderId' => $order_id ) ) )->request(); } -} \ No newline at end of file +} diff --git a/classes/class-ledyer-confirmation.php b/classes/class-ledyer-confirmation.php index ec86397..13920ec 100644 --- a/classes/class-ledyer-confirmation.php +++ b/classes/class-ledyer-confirmation.php @@ -27,19 +27,27 @@ public function actions() { add_action( 'init', array( $this, 'confirm_order' ), 10, 2 ); } - /** * Confirm the order in Woo */ public function confirm_order() { - $ledyer_confirm = filter_input( INPUT_GET, 'lco_confirm', FILTER_SANITIZE_URL ); - $order_key = filter_input( INPUT_GET, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + $ledyer_confirm = filter_input( INPUT_GET, 'lco_confirm', FILTER_SANITIZE_URL ); + $order_key = filter_input( INPUT_GET, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + $ledyer_order_id = filter_input( INPUT_GET, 'ledyer_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( empty( $ledyer_confirm ) || empty( $order_key ) ) { return; } $order_id = wc_get_order_id_by_order_key( $order_key ); + if ( empty( $order_id ) ) { + \Ledyer\Logger::log( 'Could not get the WooCommerce order id from order key ' . $order_key ); + return; + } + + $order = wc_get_order( $order_id ); + if ( empty( $order ) ) { + \Ledyer\Logger::log( 'Could not get the WooCommerce order with the id ' . $order_id ); return; } diff --git a/classes/class-ledyer-fields.php b/classes/class-ledyer-fields.php index 976ad59..62e40f8 100644 --- a/classes/class-ledyer-fields.php +++ b/classes/class-ledyer-fields.php @@ -71,7 +71,18 @@ public static function fields() { 'default' => 'yes', 'desc_tip' => true, ), - 'merchant_id' => array( + 'checkout_flow' => array( + 'title' => __( 'Checkout flow', 'ledyer-checkout-for-woocommerce' ), + 'type' => 'select', + 'options' => array( + 'embedded' => __( 'Embedded', 'ledyer-checkout-for-woocommerce' ), + 'redirect' => __( 'Redirect', 'ledyer-checkout-for-woocommerce' ), + ), + 'description' => __( 'Embedded: the checkout is embedded in the WooCommerce checkout page and partially replaces the checkout form. Redirect: the customer is redirected to a payment page hosted by Ledyer.', 'ledyer-checkout-for-woocommerce' ), + 'default' => 'embedded', + 'desc_tip' => true, + ), + 'merchant_id' => array( 'title' => __( 'Production Ledyer client ID', 'ledyer-checkout-for-woocommerce' ), 'type' => 'text', 'description' => __( 'Generate credentials (client ID and secret) in the Ledyer Merchant Portal under Settings -> API credentials.', 'ledyer-checkout-for-woocommerce' ), diff --git a/classes/class-ledyer-hpp.php b/classes/class-ledyer-hpp.php new file mode 100644 index 0000000..8ee2eca --- /dev/null +++ b/classes/class-ledyer-hpp.php @@ -0,0 +1,28 @@ +get_setting( 'testmode' ) ? 'sandbox' : 'live'; + return "https://pos.{$mode}.ledyer.com/?sessionId={$session_id}"; + } +} diff --git a/classes/class-ledyer-lco-gateway.php b/classes/class-ledyer-lco-gateway.php index 4a1785d..a5bbbda 100644 --- a/classes/class-ledyer-lco-gateway.php +++ b/classes/class-ledyer-lco-gateway.php @@ -8,6 +8,8 @@ namespace Ledyer; +use Ledyer\HPP; + \defined( 'ABSPATH' ) || die(); if ( class_exists( 'WC_Payment_Gateway' ) ) { @@ -52,7 +54,7 @@ public function __construct() { $this->testmode = 'yes' === $this->get_option( 'testmode' ); $this->logging = 'yes' === $this->get_option( 'logging' ); - \add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options')); + \add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); \add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); \add_filter( 'script_loader_tag', array( $this, 'add_data_attributes' ), 10, 2 ); @@ -60,24 +62,23 @@ public function __construct() { \add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'show_thank_you_snippet' ) ); \add_action( 'woocommerce_thankyou', 'lco_unset_sessions', 100, 1 ); - \add_action('woocommerce_admin_order_data_after_billing_address', array( $this, 'ledyer_order_billing_fields' ), 10, 1); - \add_action('woocommerce_admin_order_data_after_shipping_address', array( $this, 'ledyer_order_shipping_fields' ), 10, 1); + \add_action( 'woocommerce_admin_order_data_after_billing_address', array( $this, 'ledyer_order_billing_fields' ), 10, 1 ); + \add_action( 'woocommerce_admin_order_data_after_shipping_address', array( $this, 'ledyer_order_shipping_fields' ), 10, 1 ); // Validate shipping and billing custom fields - \add_action('woocommerce_process_shop_order_meta', array( $this, 'lom_validate_lom_edit_ledyer_order' ), 45, 1); + \add_action( 'woocommerce_process_shop_order_meta', array( $this, 'lom_validate_lom_edit_ledyer_order' ), 45, 1 ); // Save shipping and billing custom fields (higher priority than "lom_validate_lom_edit_ledyer_order" to make sure validation is done first) - \add_action( 'woocommerce_process_shop_order_meta', array( $this, 'ledyer_order_save_custom_fields' ), 50, 1); + \add_action( 'woocommerce_process_shop_order_meta', array( $this, 'ledyer_order_save_custom_fields' ), 50, 1 ); - // Invalidate token cache when settings are updated - \add_action('woocommerce_update_options', array($this, 'on_ledyer_settings_save'), 1); - } + // Invalidate token cache when settings are updated + \add_action( 'woocommerce_update_options', array( $this, 'on_ledyer_settings_save' ), 1 ); + } - public function on_ledyer_settings_save() - { - // Clear the transient to ensure fresh data is fetched on the next request - delete_transient('ledyer_token'); - delete_transient('test_ledyer_token'); - } + public function on_ledyer_settings_save() { + // Clear the transient to ensure fresh data is fetched on the next request + delete_transient( 'ledyer_token' ); + delete_transient( 'test_ledyer_token' ); + } /** * Get gateway icon. @@ -108,14 +109,14 @@ public function enqueue_scripts() { return; } - // Load the Ledyer Checkout for WooCommerce stylesheet. + // Load the Ledyer Checkout for WooCommerce stylesheet. wp_register_style( 'lco', - plugins_url('build/ledyer-checkout-for-woocommerce.css', LCO_WC_MAIN_FILE), + plugins_url( 'build/ledyer-checkout-for-woocommerce.css', LCO_WC_MAIN_FILE ), array(), - filemtime(plugin_dir_path(LCO_WC_MAIN_FILE) . ('build/ledyer-checkout-for-woocommerce.css')) + filemtime( plugin_dir_path( LCO_WC_MAIN_FILE ) . ( 'build/ledyer-checkout-for-woocommerce.css' ) ) ); - wp_enqueue_style('lco'); + wp_enqueue_style( 'lco' ); if ( ! is_checkout() ) { return; @@ -124,7 +125,7 @@ public function enqueue_scripts() { $scriptSrcUrl = 'https://checkout.live.ledyer.com/bootstrap.js'; if ( $this->testmode ) { - switch (ledyer()->get_setting( 'development_test_environment' )) { + switch ( ledyer()->get_setting( 'development_test_environment' ) ) { case 'local': case 'local-fe': $scriptSrcUrl = 'http://localhost:1337/bootstrap.iife.js'; @@ -138,16 +139,15 @@ public function enqueue_scripts() { } } - //Register iframe script + // Register iframe script wp_register_script( 'lco-iframe', $scriptSrcUrl, - array('jquery', 'wc-cart'), + array( 'jquery', 'wc-cart' ), LCO_WC_VERSION, true ); - if ( is_order_received_page() ) { wp_enqueue_script( 'lco-iframe' ); return; @@ -176,59 +176,68 @@ public function enqueue_scripts() { } } - $standard_woo_checkout_fields = apply_filters( 'lco_ignored_checkout_fields', array( - 'billing_first_name', - 'billing_last_name', - 'billing_address_1', - 'billing_address_2', - 'billing_postcode', - 'billing_city', - 'billing_phone', - 'billing_email', - 'billing_state', - 'billing_country', - 'billing_company', - 'shipping_first_name', - 'shipping_last_name', - 'shipping_address_1', - 'shipping_address_2', - 'shipping_postcode', - 'shipping_city', - 'shipping_state', - 'shipping_country', - 'shipping_company', - 'terms', - 'terms-field', - '_wp_http_referer', - 'ship_to_different_address' - ) ); + $standard_woo_checkout_fields = apply_filters( + 'lco_ignored_checkout_fields', + array( + 'billing_first_name', + 'billing_last_name', + 'billing_address_1', + 'billing_address_2', + 'billing_postcode', + 'billing_city', + 'billing_phone', + 'billing_email', + 'billing_state', + 'billing_country', + 'billing_company', + 'shipping_first_name', + 'shipping_last_name', + 'shipping_address_1', + 'shipping_address_2', + 'shipping_postcode', + 'shipping_city', + 'shipping_state', + 'shipping_country', + 'shipping_company', + 'terms', + 'terms-field', + '_wp_http_referer', + 'ship_to_different_address', + ) + ); $checkout_localize_params = array( - 'update_cart_url' => \WC_AJAX::get_endpoint( 'lco_wc_update_cart' ), - 'update_cart_nonce' => wp_create_nonce( 'lco_wc_update_cart' ), - 'change_payment_method_url' => \WC_AJAX::get_endpoint( 'lco_wc_change_payment_method' ), - 'change_payment_method_nonce' => wp_create_nonce( 'lco_wc_change_payment_method' ), - 'get_ledyer_order_url' => \WC_AJAX::get_endpoint( 'lco_wc_get_ledyer_order' ), - 'get_ledyer_order_nonce' => wp_create_nonce( 'lco_wc_get_ledyer_order' ), - 'log_to_file_url' => \WC_AJAX::get_endpoint( 'lco_wc_log_js' ), - 'log_to_file_nonce' => wp_create_nonce( 'lco_wc_log_js' ), - 'submit_order' => \WC_AJAX::get_endpoint( 'checkout' ), - 'logging' => $this->logging, - 'standard_woo_checkout_fields' => $standard_woo_checkout_fields, - 'is_confirmation_page' => ( is_lco_confirmation() ) ? 'yes' : 'no', - 'required_fields_text' => __( 'Please fill in all required checkout fields.', 'ledyer-checkout-for-woocommerce' ), - 'email_exists' => $email_exists, - 'must_login_message' => apply_filters( 'woocommerce_registration_error_email_exists', __( 'An account is already registered with your email address. Please log in.', 'woocommerce' ) ), - 'timeout_message' => __( 'Please try again, something went wrong with processing your order.', 'ledyer-checkout-for-woocommerce' ), - 'timeout_time' => apply_filters( 'lco_checkout_timeout_duration', 20 ), - 'pay_for_order' => $pay_for_order, - 'no_shipping_message' => apply_filters( 'woocommerce_no_shipping_available_html', __( 'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.', 'woocommerce' ) ), + 'update_cart_url' => \WC_AJAX::get_endpoint( 'lco_wc_update_cart' ), + 'update_cart_nonce' => wp_create_nonce( 'lco_wc_update_cart' ), + 'change_payment_method_url' => \WC_AJAX::get_endpoint( 'lco_wc_change_payment_method' ), + 'change_payment_method_nonce' => wp_create_nonce( 'lco_wc_change_payment_method' ), + 'get_ledyer_order_url' => \WC_AJAX::get_endpoint( 'lco_wc_get_ledyer_order' ), + 'get_ledyer_order_nonce' => wp_create_nonce( 'lco_wc_get_ledyer_order' ), + 'log_to_file_url' => \WC_AJAX::get_endpoint( 'lco_wc_log_js' ), + 'log_to_file_nonce' => wp_create_nonce( 'lco_wc_log_js' ), + 'submit_order' => \WC_AJAX::get_endpoint( 'checkout' ), + 'logging' => $this->logging, + 'standard_woo_checkout_fields' => $standard_woo_checkout_fields, + 'is_confirmation_page' => ( is_lco_confirmation() ) ? 'yes' : 'no', + 'required_fields_text' => __( 'Please fill in all required checkout fields.', 'ledyer-checkout-for-woocommerce' ), + 'email_exists' => $email_exists, + 'must_login_message' => apply_filters( 'woocommerce_registration_error_email_exists', __( 'An account is already registered with your email address. Please log in.', 'woocommerce' ) ), + 'timeout_message' => __( 'Please try again, something went wrong with processing your order.', 'ledyer-checkout-for-woocommerce' ), + 'timeout_time' => apply_filters( 'lco_checkout_timeout_duration', 20 ), + 'pay_for_order' => $pay_for_order, + 'no_shipping_message' => apply_filters( 'woocommerce_no_shipping_available_html', __( 'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.', 'woocommerce' ) ), ); $checkout_localize_params['force_update'] = true; wp_localize_script( 'lco', 'lco_params', $checkout_localize_params ); + wp_enqueue_script( 'lco-iframe' ); + + if ( is_wc_endpoint_url( 'order-pay' ) || 'redirect' === ( $this->settings['checkout_flow'] ?? 'embedded' ) ) { + return; + } + wp_enqueue_script( 'lco' ); } @@ -242,6 +251,14 @@ public function enqueue_scripts() { public function process_payment( $order_id ) { $order = wc_get_order( $order_id ); + // HPP Redirect flow. + if ( is_wc_endpoint_url( 'order-pay' ) || 'redirect' === ( $this->settings['checkout_flow'] ?? 'embedded' ) ) { + + // Run redirect. + return $this->hpp_redirect_handler( $order ); + + } + // Regular purchase. // 1. Process the payment. // 2. Redirect to order received page. @@ -251,7 +268,7 @@ public function process_payment( $order_id ) { 'result' => 'success', 'redirect' => add_query_arg( array( - 'lco_confirm' => 'yes', + 'lco_confirm' => 'yes', ), $order->get_checkout_order_received_url() ), @@ -261,7 +278,6 @@ public function process_payment( $order_id ) { 'result' => 'error', ); } - } /** @@ -284,7 +300,7 @@ public function process_payment_handler( $order_id ) { } if ( $order && $ledyer_order ) { - $customer_billing = isset( $ledyer_order['customer']['billingAddress'] ) ? $ledyer_order['customer']['billingAddress'] : false; + $customer_billing = isset( $ledyer_order['customer']['billingAddress'] ) ? $ledyer_order['customer']['billingAddress'] : false; $customer_shipping = isset( $ledyer_order['customer']['shippingAddress'] ) ? $ledyer_order['customer']['shippingAddress'] : false; $company_name = ! empty( $customer_billing['companyName'] ) ? $customer_billing['companyName'] : ( ! empty( $customer_shipping['companyName'] ) ? $customer_shipping['companyName'] : '' ); @@ -306,20 +322,20 @@ public function process_payment_handler( $order_id ) { $order->update_meta_data( '_wc_ledyer_country', $ledyer_country ); // Set shipping meta - if( isset( $ledyer_order['customer']['shippingAddress'] ) ) { + if ( isset( $ledyer_order['customer']['shippingAddress'] ) ) { $order->update_meta_data( '_shipping_attention_name', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['attentionName'] ) ); $order->update_meta_data( '_shipping_care_of', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['careOf'] ) ); } // Set order recipient meta - if( isset( $ledyer_order['customer']['shippingAddress']['contact'] ) ) { - $order->update_meta_data( '_shipping_first_name', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']["firstName"] ) ); - $order->update_meta_data( '_shipping_last_name', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']["lastName"]) ); - $order->update_meta_data( '_shipping_phone', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']["phone"] ) ); - $order->update_meta_data( '_shipping_email', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']["email"]) ); + if ( isset( $ledyer_order['customer']['shippingAddress']['contact'] ) ) { + $order->update_meta_data( '_shipping_first_name', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']['firstName'] ) ); + $order->update_meta_data( '_shipping_last_name', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']['lastName'] ) ); + $order->update_meta_data( '_shipping_phone', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']['phone'] ) ); + $order->update_meta_data( '_shipping_email', sanitize_text_field( $ledyer_order['customer']['shippingAddress']['contact']['email'] ) ); } // Set billing meta - if( isset( $ledyer_order['customer']['billingAddress'] ) ) { + if ( isset( $ledyer_order['customer']['billingAddress'] ) ) { $order->update_meta_data( '_billing_attention_name', sanitize_text_field( $ledyer_order['customer']['billingAddress']['attentionName'] ) ); $order->update_meta_data( '_billing_care_of', sanitize_text_field( $ledyer_order['customer']['billingAddress']['careOf'] ) ); } @@ -339,6 +355,61 @@ public function process_payment_handler( $order_id ) { return false; } + /** + * Process the payment for HPP/redirect checkout flow. + * + * @param object $order The WooCommerce order. + * + * @return array|string[] + */ + protected function hpp_redirect_handler( $order ) { + + if ( empty( $order ) ) { + + return array( + 'result' => 'error', + 'messages' => array( 'Failed to get order for HPP.' ), + ); + } + + $data = \Ledyer\Requests\Helpers\Woocommerce_Bridge::get_order_data( $order ); + // Add confirmation URL to the order. + $ledyer_order = ledyer()->api->create_order_session( $data ); + if ( is_wp_error( $ledyer_order ) ) { + + return array( + 'result' => 'error', + 'messages' => array( 'Failed to create order session for HPP.' ), + ); + } + + // Create a HPP url. + $hpp = new HPP(); + $hpp_redirect = $hpp->create_hpp_url( $ledyer_order['sessionId'] ); + + // Set WC order transaction ID. + $order->update_meta_data( '_wc_ledyer_order_id', $ledyer_order['orderId'] ); + $order->update_meta_data( '_wc_ledyer_session_id', $ledyer_order['sessionId'] ); + + $order->set_transaction_id( $ledyer_order['orderId'] ); + + $environment = $this->testmode ? 'sandbox' : 'production'; + $order->update_meta_data( '_wc_ledyer_environment', $environment ); + + $ledyer_country = wc_get_base_location()['country']; + $order->update_meta_data( '_wc_ledyer_country', $ledyer_country ); + + $order->save(); + + // All good. Redirect customer to ledyer Hosted payment page. + $order->add_order_note( __( 'Customer redirected to Ledyer Hosted Payment Page.', 'ledyer-checkout-for-woocommerce' ) ); + + return array( + 'result' => 'success', + 'redirect' => $hpp_redirect, + ); + } + /** * This plugin doesn't handle order management, but it allows Ledyer Order Management plugin to process refunds * and then return true or false. @@ -364,10 +435,10 @@ public function show_thank_you_snippet( $order_id = null ) { if ( is_object( $order ) && $order->get_transaction_id() ) { ?> -