diff --git a/changelog.txt b/changelog.txt index bd4294893..089810461 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,19 @@ *** Changelog *** += 2.6.1 - xxxx-xx-xx = +* Fix - Payment tokens fixes and adjustments #2106 +* Fix - Pay upon Invoice: Add input validation to Experience Context fields #2092 +* Fix - Disable markup in get_plugin_data() returns to fix an issue with wptexturize() #2094 +* Enhancement - Pay later messaging configurator improvements #2107 +* Enhancement - Replace the middleware URL from connect.woocommerce.com to api.woocommerce.com/integrations #2130 +* Enhancement - Remove all Sofort references as it has been deprecated #2124 +* Enhancement - Improve funding source names #2118 +* Enhancement - More fraud prevention capabilities by storing additional data in the order #2125 +* Enhancement - Update ACDC currency eligibility for AMEX #2129 +* Enhancement - Sync shipping options with Venmo when skipping final confirmation on Checkout #2108 +* Enhancement - Card Fields: Add a filter for the CVC field and update the placeholder to match the label #2089 +* Enhancement - Product Title: Sanitize before sending to PayPal #2090 + = 2.6.0 - 2024-03-20 = * Fix - invoice_id not included in API call when creating payment with saved card #2086 * Fix - Typo in SCA indicators for ACDC Vault transactions #2083 diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index e2c64ac0c..22ff4e208 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -1000,11 +1000,21 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages if ( $this->settings->has( '3d_secure_contingency' ) ) { $value = $this->settings->get( '3d_secure_contingency' ); if ( $value ) { - return $value; + return $this->return_3ds_contingency( $value ); } } - return 'SCA_WHEN_REQUIRED'; + return $this->return_3ds_contingency( 'SCA_WHEN_REQUIRED' ); + } + + /** + * Processes and returns the 3D Secure contingency. + * + * @param string $contingency The ThreeD secure contingency. + * @return string + */ + private function return_3ds_contingency( string $contingency ): string { + return apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $contingency ); } /** diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index c286b9f20..1ef06c3be 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -329,6 +329,21 @@ class CreateOrderEndpoint implements EndpointInterface { if ( 'pay-now' === $data['context'] && is_a( $wc_order, \WC_Order::class ) ) { $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); + + $payment_source = $order->payment_source(); + $payment_source_name = $payment_source ? $payment_source->name() : null; + $payer = $order->payer(); + if ( + $payer + && $payment_source_name + && in_array( $payment_source_name, PayPalGateway::PAYMENT_SOURCES_WITH_PAYER_EMAIL, true ) + ) { + $payer_email = $payer->email_address(); + if ( $payer_email ) { + $wc_order->update_meta_data( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY, $payer_email ); + } + } + $wc_order->save_meta_data(); do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); diff --git a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php index 03d8fbeed..46b4ddf41 100644 --- a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php +++ b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php @@ -159,6 +159,22 @@ class EarlyOrderHandler { $wc_order = wc_get_order( $order_id ); $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); + + $payment_source = $order->payment_source(); + $payment_source_name = $payment_source ? $payment_source->name() : null; + $payer = $order->payer(); + if ( + $payer + && $payment_source_name + && in_array( $payment_source_name, PayPalGateway::PAYMENT_SOURCES_WITH_PAYER_EMAIL, true ) + && $wc_order instanceof \WC_Order + ) { + $payer_email = $payer->email_address(); + if ( $payer_email ) { + $wc_order->update_meta_data( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY, $payer_email ); + } + } + $wc_order->save_meta_data(); /** diff --git a/modules/ppcp-button/src/Helper/ThreeDSecure.php b/modules/ppcp-button/src/Helper/ThreeDSecure.php index 66c89e4be..1991112b5 100644 --- a/modules/ppcp-button/src/Helper/ThreeDSecure.php +++ b/modules/ppcp-button/src/Helper/ThreeDSecure.php @@ -57,21 +57,24 @@ class ThreeDSecure { * * @link https://developer.paypal.com/docs/business/checkout/add-capabilities/3d-secure/#authenticationresult * - * @param Order $order The order for which the decission is needed. + * @param Order $order The order for which the decision is needed. * * @return int */ public function proceed_with_order( Order $order ): int { + + do_action( 'woocommerce_paypal_payments_three_d_secure_before_check', $order ); + $payment_source = $order->payment_source(); if ( ! $payment_source ) { - return self::NO_DECISION; + return $this->return_decision( self::NO_DECISION, $order ); } if ( ! ( $payment_source->properties()->brand ?? '' ) ) { - return self::NO_DECISION; + return $this->return_decision( self::NO_DECISION, $order ); } if ( ! ( $payment_source->properties()->authentication_result ?? '' ) ) { - return self::NO_DECISION; + return $this->return_decision( self::NO_DECISION, $order ); } $authentication_result = $payment_source->properties()->authentication_result ?? null; @@ -81,18 +84,31 @@ class ThreeDSecure { $this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) ); if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) { - return self::PROCCEED; + return $this->return_decision( self::PROCCEED, $order ); } if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) { - return self::RETRY; + return $this->return_decision( self::RETRY, $order ); } if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) { - return $this->no_liability_shift( $result ); + return $this->return_decision( $this->no_liability_shift( $result ), $order ); } } - return self::NO_DECISION; + return $this->return_decision( self::NO_DECISION, $order ); + } + + /** + * Processes and returns a ThreeD secure decision. + * + * @param int $decision The ThreeD secure decision. + * @param Order $order The PayPal Order object. + * @return int + */ + public function return_decision( int $decision, Order $order ) { + $decision = apply_filters( 'woocommerce_paypal_payments_three_d_secure_decision', $decision, $order ); + do_action( 'woocommerce_paypal_payments_three_d_secure_after_check', $order, $decision ); + return $decision; } /** diff --git a/modules/ppcp-card-fields/src/CardFieldsModule.php b/modules/ppcp-card-fields/src/CardFieldsModule.php index 213134857..6835108c3 100644 --- a/modules/ppcp-card-fields/src/CardFieldsModule.php +++ b/modules/ppcp-card-fields/src/CardFieldsModule.php @@ -115,17 +115,19 @@ class CardFieldsModule implements ModuleInterface { $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); + $three_d_secure_contingency = + $settings->has( '3d_secure_contingency' ) + ? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) ) + : ''; + if ( - $settings->has( '3d_secure_contingency' ) - && ( - $settings->get( '3d_secure_contingency' ) === 'SCA_ALWAYS' - || $settings->get( '3d_secure_contingency' ) === 'SCA_WHEN_REQUIRED' - ) + $three_d_secure_contingency === 'SCA_ALWAYS' + || $three_d_secure_contingency === 'SCA_WHEN_REQUIRED' ) { $data['payment_source']['card'] = array( 'attributes' => array( 'verification' => array( - 'method' => $settings->get( '3d_secure_contingency' ), + 'method' => $three_d_secure_contingency, ), ), ); diff --git a/modules/ppcp-paylater-block/resources/js/edit.js b/modules/ppcp-paylater-block/resources/js/edit.js index c702d0ad9..44b4f895d 100644 --- a/modules/ppcp-paylater-block/resources/js/edit.js +++ b/modules/ppcp-paylater-block/resources/js/edit.js @@ -194,12 +194,11 @@ export default function Edit( { attributes, clientId, setAttributes } ) { help={ __( 'Used for the analytics dashboard in the merchant account.', 'woocommerce-paypal-payments' ) } options={ [ { label: __( 'Detect automatically', 'woocommerce-paypal-payments' ), value: 'auto' }, + { label: __( 'Product Page', 'woocommerce-paypal-payments' ), value: 'product' }, { label: __( 'Cart', 'woocommerce-paypal-payments' ), value: 'cart' }, - { label: __( 'Payment', 'woocommerce-paypal-payments' ), value: 'payment' }, - { label: __( 'Product', 'woocommerce-paypal-payments' ), value: 'product' }, - { label: __( 'Product list', 'woocommerce-paypal-payments' ), value: 'product-list' }, + { label: __( 'Checkout', 'woocommerce-paypal-payments' ), value: 'checkout' }, { label: __( 'Home', 'woocommerce-paypal-payments' ), value: 'home' }, - { label: __( 'Category', 'woocommerce-paypal-payments' ), value: 'category' }, + { label: __( 'Shop', 'woocommerce-paypal-payments' ), value: 'shop' }, ] } value={ placement } onChange={ ( value ) => setAttributes( { placement: value } ) } diff --git a/modules/ppcp-paylater-block/src/PayLaterBlockModule.php b/modules/ppcp-paylater-block/src/PayLaterBlockModule.php index d6b9c7404..223ccd972 100644 --- a/modules/ppcp-paylater-block/src/PayLaterBlockModule.php +++ b/modules/ppcp-paylater-block/src/PayLaterBlockModule.php @@ -40,7 +40,7 @@ class PayLaterBlockModule implements ModuleInterface { * @return bool true if the block is enabled, otherwise false. */ public static function is_block_enabled( SettingsStatus $settings_status ): bool { - return self::is_module_loading_required() && $settings_status->is_pay_later_messaging_enabled_for_location( 'woocommerceBlock' ); + return self::is_module_loading_required() && $settings_status->is_pay_later_messaging_enabled_for_location( 'custom_placement' ); } /** diff --git a/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js b/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js index 15f65f183..9549ef2af 100644 --- a/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js +++ b/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js @@ -29,21 +29,17 @@ document.addEventListener( 'DOMContentLoaded', () => { setTimeout(() => { saveChangesButton.click(); // Trigger click event on saveChangesButton isSaving = false; // Reset flag when saving is complete - }, 500); // Adjust the delay as needed + }, 1000); // Adjust the delay as needed } }); - merchantConfigurators.Messaging({ config: PcpPayLaterConfigurator.config, merchantClientId: PcpPayLaterConfigurator.merchantClientId, partnerClientId: PcpPayLaterConfigurator.partnerClientId, partnerName: 'WooCommerce', bnCode: 'Woo_PPCP', - placements: ['cart', 'checkout', 'product', 'shop', 'home'], - custom_placement:[{ - message_reference: 'woocommerceBlock', - }], + placements: ['cart', 'checkout', 'product', 'shop', 'home', 'custom_placement'], styleOverrides: { button: publishButtonClassName, header: PcpPayLaterConfigurator.headerClassName, diff --git a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php index 6584b7da3..df4e4affa 100644 --- a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php +++ b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php @@ -99,10 +99,13 @@ class SaveConfig { $this->settings->set( 'pay_later_messaging_enabled', true ); $enabled_locations = array(); - foreach ( $config as $placement => $data ) { $this->save_config_for_location( $data, $placement ); + if ( $placement === 'custom_placement' ) { + $data = $data[0] ?? array(); + } + if ( $data['status'] === 'enabled' ) { $enabled_locations[] = $placement; } @@ -129,6 +132,7 @@ class SaveConfig { $this->set_value_if_present( $config, 'logo-type', "pay_later_{$location}_message_logo" ); $this->set_value_if_present( $config, 'logo-color', "pay_later_{$location}_message_color" ); $this->set_value_if_present( $config, 'text-size', "pay_later_{$location}_message_text_size" ); + $this->set_value_if_present( $config, 'text-color', "pay_later_{$location}_message_color" ); } /** diff --git a/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php b/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php index d36fe91c3..c73bbd0ab 100644 --- a/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php +++ b/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php @@ -27,7 +27,7 @@ class ConfigFactory { 'product' => $this->for_location( $settings, 'product' ), 'shop' => $this->for_location( $settings, 'shop' ), 'home' => $this->for_location( $settings, 'home' ), - 'woocommerceBlock' => $this->for_location( $settings, 'woocommerceBlock' ), + 'custom_placement' => array( $this->for_location( $settings, 'woocommerceBlock' ) ), ); } @@ -40,29 +40,87 @@ class ConfigFactory { private function for_location( Settings $settings, string $location ): array { $selected_locations = $settings->has( 'pay_later_messaging_locations' ) ? $settings->get( 'pay_later_messaging_locations' ) : array(); - if ( in_array( $location, array( 'shop', 'home' ), true ) ) { - $config = array( - 'layout' => $this->get_or_default( $settings, "pay_later_{$location}_message_layout", 'flex' ), - 'color' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_color", 'black' ), - 'ratio' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_ratio", '8x1' ), - ); - } elseif ( $location !== 'woocommerceBlock' ) { - $config = array( - 'layout' => $this->get_or_default( $settings, "pay_later_{$location}_message_layout", 'text' ), - 'logo-position' => $this->get_or_default( $settings, "pay_later_{$location}_message_position", 'left' ), - 'logo-type' => $this->get_or_default( $settings, "pay_later_{$location}_message_logo", 'inline' ), - 'text-color' => $this->get_or_default( $settings, "pay_later_{$location}_message_color", 'black' ), - 'text-size' => $this->get_or_default( $settings, "pay_later_{$location}_message_text_size", '12' ), - - ); + switch ( $location ) { + case 'shop': + case 'home': + $config = $this->for_shop_or_home( $settings, $location, $selected_locations ); + break; + case 'woocommerceBlock': + $config = $this->for_woocommerce_block( $selected_locations ); + break; + default: + $config = $this->for_default_location( $settings, $location, $selected_locations ); + break; } - return array_merge( - array( - 'status' => in_array( $location, $selected_locations, true ) ? 'enabled' : 'disabled', - 'placement' => $location, - ), - $config ?? array() + return $config; + } + + /** + * Returns the configurator config for shop, home locations. + * + * @param Settings $settings The settings. + * @param string $location The location. + * @param string[] $selected_locations The list of selected locations. + * @return array{ + * layout: string, + * color: string, + * ratio: string, + * status: "disabled"|"enabled", + * placement: string + * } The configurator config map. + */ + private function for_shop_or_home( Settings $settings, string $location, array $selected_locations ): array { + return array( + 'layout' => $this->get_or_default( $settings, "pay_later_{$location}_message_layout", 'flex' ), + 'color' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_color", 'black' ), + 'ratio' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_ratio", '8x1' ), + 'status' => in_array( $location, $selected_locations, true ) ? 'enabled' : 'disabled', + 'placement' => $location, + ); + } + + /** + * Returns the configurator config for woocommerceBlock location. + * + * @param array $selected_locations The list of selected locations. + * @return array{ + * status: "disabled"|"enabled", + * message_reference: string + * } The configurator config map. + */ + private function for_woocommerce_block( array $selected_locations ): array { + return array( + 'status' => in_array( 'custom_placement', $selected_locations, true ) ? 'enabled' : 'disabled', + 'message_reference' => 'woocommerceBlock', + ); + } + + /** + * Returns the configurator config for default locations. + * + * @param Settings $settings The settings. + * @param string $location The location. + * @param string[] $selected_locations The list of selected locations. + * @return array{ + * layout: string, + * logo-position: string, + * logo-type: string, + * text-color: string, + * text-size: string, + * status: "disabled"|"enabled", + * placement: string + * } The configurator config map. + */ + private function for_default_location( Settings $settings, string $location, array $selected_locations ): array { + return array( + 'layout' => $this->get_or_default( $settings, "pay_later_{$location}_message_layout", 'text' ), + 'logo-position' => $this->get_or_default( $settings, "pay_later_{$location}_message_position", 'left' ), + 'logo-type' => $this->get_or_default( $settings, "pay_later_{$location}_message_logo", 'inline' ), + 'text-color' => $this->get_or_default( $settings, "pay_later_{$location}_message_color", 'black' ), + 'text-size' => $this->get_or_default( $settings, "pay_later_{$location}_message_text_size", '12' ), + 'status' => in_array( $location, $selected_locations, true ) ? 'enabled' : 'disabled', + 'placement' => $location, ); } @@ -73,9 +131,9 @@ class ConfigFactory { * @param string $key The key. * @param mixed $default The default value. * @param array|null $allowed_values The list of allowed values, or null if all values are allowed. - * @return mixed + * @return string */ - private function get_or_default( Settings $settings, string $key, $default, ?array $allowed_values = null ) { + private function get_or_default( Settings $settings, string $key, $default, ?array $allowed_values = null ): string { if ( $settings->has( $key ) ) { $value = $settings->get( $key ); if ( ! $allowed_values || in_array( $value, $allowed_values, true ) ) { diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index f93a75a66..c8240c2c7 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -281,7 +281,11 @@ class SavePaymentMethodsModule implements ModuleInterface { $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - $verification_method = $settings->has( '3d_secure_contingency' ) ? $settings->get( '3d_secure_contingency' ) : ''; + + $verification_method = + $settings->has( '3d_secure_contingency' ) + ? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) ) + : ''; $change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 73afb085b..19ebe9d08 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -48,12 +48,18 @@ class PayPalGateway extends \WC_Payment_Gateway { const ORDER_ID_META_KEY = '_ppcp_paypal_order_id'; const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode'; const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source'; + const ORDER_PAYER_EMAIL_META_KEY = '_ppcp_paypal_payer_email'; const FEES_META_KEY = '_ppcp_paypal_fees'; const REFUND_FEES_META_KEY = '_ppcp_paypal_refund_fees'; const REFUNDS_META_KEY = '_ppcp_refunds'; const THREE_D_AUTH_RESULT_META_KEY = '_ppcp_paypal_3DS_auth_result'; const FRAUD_RESULT_META_KEY = '_ppcp_paypal_fraud_result'; + /** + * List of payment sources wich we are expected to store the payer email in the WC Order metadata. + */ + const PAYMENT_SOURCES_WITH_PAYER_EMAIL = array( 'paypal', 'paylater', 'venmo' ); + /** * The Settings Renderer. * diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php index 32569c60b..4cd9e07ed 100644 --- a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php @@ -76,7 +76,7 @@ trait CreditCardOrderInfoHandlingTrait { /** * Fired when the 3DS information is added to WC order. */ - do_action( 'woocommerce_paypal_payments_thee_d_secure_added', $wc_order, $order ); + do_action( 'woocommerce_paypal_payments_three_d_secure_added', $wc_order, $order ); } } @@ -96,8 +96,9 @@ trait CreditCardOrderInfoHandlingTrait { return; } - $fraud_responses = $fraud->to_array(); - $card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' ); + $fraud_responses = $fraud->to_array(); + $card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' ); + $card_last_digits = $payment_source->properties()->last_digits ?? __( 'N/A', 'woocommerce-paypal-payments' ); $avs_response_order_note_title = __( 'Address Verification Result', 'woocommerce-paypal-payments' ); /* translators: %1$s is AVS order note title, %2$s is AVS order note result markup */ @@ -109,6 +110,7 @@ trait CreditCardOrderInfoHandlingTrait {