diff --git a/api/order-functions.php b/api/order-functions.php index 340be8585..cbfa42968 100644 --- a/api/order-functions.php +++ b/api/order-functions.php @@ -87,6 +87,32 @@ function ppcp_capture_order( WC_Order $wc_order ): void { } } +/** + * Reauthorizes the PayPal order. + * + * @param WC_Order $wc_order The WC order. + * @throws InvalidArgumentException When the order cannot be captured. + * @throws Exception When the operation fails. + */ +function ppcp_reauthorize_order( WC_Order $wc_order ): void { + $intent = strtoupper( (string) $wc_order->get_meta( PayPalGateway::INTENT_META_KEY ) ); + + if ( $intent !== 'AUTHORIZE' ) { + throw new InvalidArgumentException( 'Only orders with "authorize" intent can be reauthorized.' ); + } + $captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) ); + if ( $captured ) { + throw new InvalidArgumentException( 'The order is already captured.' ); + } + + $authorized_payment_processor = PPCP::container()->get( 'wcgateway.processor.authorized-payments' ); + assert( $authorized_payment_processor instanceof AuthorizedPaymentsProcessor ); + + if ( $authorized_payment_processor->reauthorize_payment( $wc_order ) !== AuthorizedPaymentsProcessor::SUCCESSFUL ) { + throw new RuntimeException( $authorized_payment_processor->reauthorization_failure_reason() ?: 'Reauthorization failed.' ); + } +} + /** * Refunds the PayPal order. * Note that you can use wc_refund_payment() to trigger the refund in WC and PayPal. diff --git a/changelog.txt b/changelog.txt index 86251c280..ad4695527 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,40 @@ *** Changelog *** += 2.6.1 - 2024-04-09 = +* 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 +* Fix - Problem changing the shipping option in block pages #2142 +* Fix - Saved payment token deleted after payment with another saved payment token #2146 +* 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 +* Enhancement - Add filter for disabling permit_multiple_payment_tokens vault attribute #2136 +* Enhancement - Filter to hide PayPal email address not working on order detail #2137 + += 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 +* Fix - Payments with saved card tokens use Capture intent when Authorize is configured #2069 +* Fix - WooPayments multi-currency causing currency mismatch error on Block Cart & Checkout pages #2054 +* Fix - "Must pass createSubscription with intent=subscription" error with PayPal Subscriptions mode #2058 +* Fix - "Proceed to PayPal" button displayed for Free trial PayPal Subscription products when payment token is saved #2041 +* Fix - ACDC payments with new credit card may fail when debugging is enabled (JSON malformed by warning) #2051 +* Enhancement - Add Pay Later Messaging block #1897 +* Enhancement - Submit the form instead of refreshing the page to show the save notice #2081 +* Enhancement - Integrate pay later messaging block with the messaging configurator #2080 +* Enhancement - Reauthorize authorized payments #2062 +* Enhancement - Do not handle VAULT.PAYMENT-TOKEN.CREATED webhook for Vault v3 #2079 +* Enhancement - Improve the messaging configurator styles #2053 +* Enhancement - Ensure PayPal Vaulting is not selected as Subscriptions Mode when Reference Transactions are disabled #2057 +* Enhancement - Pay later messaging configurator & messaging block adjustments #2096 + = 2.5.4 - 2024-02-27 = * Fix - Cannot enable Apple Pay when API credentials were manually created #2015 * Fix - Cart simulation type error #1943 diff --git a/modules.php b/modules.php index 8ee91f43e..c6840d8ab 100644 --- a/modules.php +++ b/modules.php @@ -68,7 +68,7 @@ return function ( string $root_dir ): iterable { $modules[] = ( require "$modules_dir/ppcp-save-payment-methods/module.php" )(); } - if ( PayLaterBlockModule::is_enabled() ) { + if ( PayLaterBlockModule::is_module_loading_required() ) { $modules[] = ( require "$modules_dir/ppcp-paylater-block/module.php" )(); } diff --git a/modules/ppcp-admin-notices/src/AdminNotices.php b/modules/ppcp-admin-notices/src/AdminNotices.php index 73fe24b71..0489df02c 100644 --- a/modules/ppcp-admin-notices/src/AdminNotices.php +++ b/modules/ppcp-admin-notices/src/AdminNotices.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\AdminNotices; +use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; +use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; @@ -40,6 +42,34 @@ class AdminNotices implements ModuleInterface { $renderer->render(); } ); + + add_action( + Repository::NOTICES_FILTER, + /** + * Adds persisted notices to the notices array. + * + * @param array $notices The notices. + * @return array + * + * @psalm-suppress MissingClosureParamType + */ + function ( $notices ) use ( $c ) { + if ( ! is_array( $notices ) ) { + return $notices; + } + + $admin_notices = $c->get( 'admin-notices.repository' ); + assert( $admin_notices instanceof Repository ); + + $persisted_notices = $admin_notices->get_persisted_and_clear(); + + if ( $persisted_notices ) { + $notices = array_merge( $notices, $persisted_notices ); + } + + return $notices; + } + ); } /** diff --git a/modules/ppcp-admin-notices/src/Entity/Message.php b/modules/ppcp-admin-notices/src/Entity/Message.php index 0e8bc959e..97624d872 100644 --- a/modules/ppcp-admin-notices/src/Entity/Message.php +++ b/modules/ppcp-admin-notices/src/Entity/Message.php @@ -92,4 +92,18 @@ class Message { public function wrapper(): string { return $this->wrapper; } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array(): array { + return array( + 'type' => $this->type, + 'message' => $this->message, + 'dismissable' => $this->dismissable, + 'wrapper' => $this->wrapper, + ); + } } diff --git a/modules/ppcp-admin-notices/src/Repository/Repository.php b/modules/ppcp-admin-notices/src/Repository/Repository.php index e13a5dd6c..9573a81a4 100644 --- a/modules/ppcp-admin-notices/src/Repository/Repository.php +++ b/modules/ppcp-admin-notices/src/Repository/Repository.php @@ -16,7 +16,8 @@ use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; */ class Repository implements RepositoryInterface { - const NOTICES_FILTER = 'ppcp.admin-notices.current-notices'; + const NOTICES_FILTER = 'ppcp.admin-notices.current-notices'; + const PERSISTED_NOTICES_OPTION = 'woocommerce_ppcp-admin-notices'; /** * Returns the current messages. @@ -37,4 +38,40 @@ class Repository implements RepositoryInterface { } ); } + + /** + * Adds a message to persist between page reloads. + * + * @param Message $message The message. + * @return void + */ + public function persist( Message $message ): void { + $persisted_notices = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array(); + + $persisted_notices[] = $message->to_array(); + + update_option( self::PERSISTED_NOTICES_OPTION, $persisted_notices ); + } + + /** + * Adds a message to persist between page reloads. + * + * @return array|Message[] + */ + public function get_persisted_and_clear(): array { + $notices = array(); + + $persisted_data = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array(); + foreach ( $persisted_data as $notice_data ) { + $notices[] = new Message( + (string) ( $notice_data['message'] ?? '' ), + (string) ( $notice_data['type'] ?? '' ), + (bool) ( $notice_data['dismissable'] ?? true ), + (string) ( $notice_data['wrapper'] ?? '' ) + ); + } + + update_option( self::PERSISTED_NOTICES_OPTION, array(), true ); + return $notices; + } } diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 0ae8ed7af..225530ec2 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -1408,32 +1408,32 @@ return array( 'BE' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'USD', 'CAD' ), + 'amex' => array(), ), 'BG' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'CY' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'CZ' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'CZK' ), + 'amex' => array(), ), 'DE' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'DK' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'DKK' ), + 'amex' => array(), ), 'EE' => array( 'mastercard' => array(), @@ -1443,32 +1443,32 @@ return array( 'ES' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'FI' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'FR' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'GB' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'GBP', 'USD' ), + 'amex' => array(), ), 'GR' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'HU' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'HUF' ), + 'amex' => array(), ), 'IE' => array( 'mastercard' => array(), @@ -1478,7 +1478,7 @@ return array( 'IT' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'US' => array( 'mastercard' => array(), @@ -1489,7 +1489,7 @@ return array( 'CA' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'CAD' ), + 'amex' => array( 'CAD', 'USD' ), 'jcb' => array( 'CAD' ), ), 'LI' => array( @@ -1500,22 +1500,22 @@ return array( 'LT' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'LU' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'LV' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'USD' ), + 'amex' => array(), ), 'MT' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'MX' => array( 'mastercard' => array(), @@ -1525,7 +1525,7 @@ return array( 'NL' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'USD' ), + 'amex' => array(), ), 'NO' => array( 'mastercard' => array(), @@ -1535,32 +1535,32 @@ return array( 'PL' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'USD', 'GBP', 'PLN' ), + 'amex' => array(), ), 'PT' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'USD', 'CAD', 'GBP' ), + 'amex' => array(), ), 'RO' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'USD' ), + 'amex' => array(), ), 'SE' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'SEK' ), + 'amex' => array(), ), 'SI' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR' ), + 'amex' => array(), ), 'SK' => array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array( 'EUR', 'GBP' ), + 'amex' => array(), ), 'JP' => array( 'mastercard' => array(), diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php index ab8d5473b..538ca224d 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php @@ -115,7 +115,7 @@ class PaymentMethodTokensEndpoint { * @throws RuntimeException When something when wrong with the request. * @throws PayPalApiException When something when wrong setting up the token. */ - public function payment_tokens( PaymentSource $payment_source ): stdClass { + public function create_payment_token( PaymentSource $payment_source ): stdClass { $data = array( 'payment_source' => array( $payment_source->name() => $payment_source->properties(), diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php index bfe05d1b5..284bc887f 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; +use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WP_Error; @@ -92,4 +93,53 @@ class PaymentTokensEndpoint { throw new PayPalApiException( $json, $status_code ); } } + + /** + * Returns all payment tokens for the given customer. + * + * @param string $customer_id PayPal customer id. + * @return array + * + * @throws RuntimeException When something went wrong with the request. + * @throws PayPalApiException When something went wrong getting the payment tokens. + */ + public function payment_tokens_for_customer( string $customer_id ): array { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens?customer_id=' . $customer_id; + $args = array( + 'method' => 'GET', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + ); + + $response = $this->request( $url, $args ); + if ( $response instanceof WP_Error ) { + throw new RuntimeException( $response->get_error_message() ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( $json, $status_code ); + } + + $tokens = array(); + $payment_tokens = $json->payment_tokens ?? array(); + foreach ( $payment_tokens as $payment_token ) { + $name = array_key_first( (array) $payment_token->payment_source ) ?? ''; + if ( $name ) { + $tokens[] = array( + 'id' => $payment_token->id, + 'payment_source' => new PaymentSource( + $name, + $payment_token->payment_source->$name + ), + ); + } + } + + return $tokens; + } } diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php index 210beae16..484b89a94 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php @@ -193,6 +193,53 @@ class PaymentsEndpoint { return $this->capture_factory->from_paypal_response( $json ); } + /** + * Reauthorizes an order. + * + * @param string $authorization_id The id. + * @param Money|null $amount The amount to capture. If not specified, the whole authorized amount is captured. + * + * @return string + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function reauthorize( string $authorization_id, ?Money $amount = null ) : string { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/reauthorize'; + + $data = array(); + if ( $amount ) { + $data['amount'] = $amount->to_array(); + } + + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $data, JSON_FORCE_OBJECT ), + ); + + $response = $this->request( $url, $args ); + $json = json_decode( $response['body'] ); + + if ( is_wp_error( $response ) ) { + throw new RuntimeException( 'Could not reauthorize authorized payment.' ); + } + + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 201 !== $status_code || ! is_object( $json ) ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $json->id; + } + /** * Refunds a payment. * diff --git a/modules/ppcp-api-client/src/Factory/ItemFactory.php b/modules/ppcp-api-client/src/Factory/ItemFactory.php index e8fc98c86..c2b14d0e1 100644 --- a/modules/ppcp-api-client/src/Factory/ItemFactory.php +++ b/modules/ppcp-api-client/src/Factory/ItemFactory.php @@ -61,10 +61,10 @@ class ItemFactory { $price = (float) $item['line_subtotal'] / (float) $item['quantity']; return new Item( - mb_substr( $product->get_name(), 0, 127 ), + $this->prepare_item_string( $product->get_name() ), new Money( $price, $this->currency ), $quantity, - $this->prepare_description( $product->get_description() ), + $this->prepare_item_string( $product->get_description() ), null, $this->prepare_sku( $product->get_sku() ), ( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, @@ -138,10 +138,10 @@ class ItemFactory { $image = $product instanceof WC_Product ? wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ) : ''; return new Item( - mb_substr( $item->get_name(), 0, 127 ), + $this->prepare_item_string( $item->get_name() ), new Money( $price_without_tax_rounded, $currency ), $quantity, - $product instanceof WC_Product ? $this->prepare_description( $product->get_description() ) : '', + $product instanceof WC_Product ? $this->prepare_item_string( $product->get_description() ) : '', null, $product instanceof WC_Product ? $this->prepare_sku( $product->get_sku() ) : '', ( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, @@ -160,7 +160,7 @@ class ItemFactory { */ private function from_wc_order_fee( \WC_Order_Item_Fee $item, \WC_Order $order ): Item { return new Item( - $item->get_name(), + $this->prepare_item_string( $item->get_name() ), new Money( (float) $item->get_amount(), $order->get_currency() ), $item->get_quantity(), '', diff --git a/modules/ppcp-api-client/src/Helper/ItemTrait.php b/modules/ppcp-api-client/src/Helper/ItemTrait.php index 3b1e05bd0..289889290 100644 --- a/modules/ppcp-api-client/src/Helper/ItemTrait.php +++ b/modules/ppcp-api-client/src/Helper/ItemTrait.php @@ -12,14 +12,14 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Helper; trait ItemTrait { /** - * Cleanups the description and prepares it for sending to PayPal. + * Cleans up item strings (title and description for example) and prepares them for sending to PayPal. * - * @param string $description Item description. + * @param string $string Item string. * @return string */ - protected function prepare_description( string $description ): string { - $description = strip_shortcodes( wp_strip_all_tags( $description ) ); - return substr( $description, 0, 127 ) ?: ''; + protected function prepare_item_string( string $string ): string { + $string = strip_shortcodes( wp_strip_all_tags( $string ) ); + return substr( $string, 0, 127 ) ?: ''; } /** diff --git a/modules/ppcp-blocks/resources/js/Helper/Helper.js b/modules/ppcp-blocks/resources/js/Helper/Helper.js new file mode 100644 index 000000000..379c88a49 --- /dev/null +++ b/modules/ppcp-blocks/resources/js/Helper/Helper.js @@ -0,0 +1,22 @@ +/** + * @param str + * @returns {string} + */ +export const toSnakeCase = (str) => { + return str.replace(/[\w]([A-Z])/g, function(m) { + return m[0] + "_" + m[1]; + }).toLowerCase(); +} + +/** + * @param obj + * @returns {{}} + */ +export const convertKeysToSnakeCase = (obj) => { + const newObj = {}; + Object.keys(obj).forEach((key) => { + const newKey = toSnakeCase(key); + newObj[newKey] = obj[key]; + }); + return newObj; +} diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 5e6450045..dd324bd2c 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -6,6 +6,9 @@ import { paypalOrderToWcAddresses, paypalSubscriptionToWcAddresses } from "./Helper/Address"; +import { + convertKeysToSnakeCase +} from "./Helper/Helper"; import { cartHasSubscriptionProducts, isPayPalSubscription @@ -18,6 +21,7 @@ import { } from '../../../ppcp-button/resources/js/modules/Helper/Style' import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap"; +import {keysToCamelCase} from "../../../ppcp-button/resources/js/modules/Helper/Utils"; const config = wc.wcSettings.getSetting('ppcp-gateway_data'); window.ppcpFundingSource = config.fundingSource; @@ -286,17 +290,43 @@ const PayPalComponent = ({ onClick(); }; - let handleShippingChange = null; - let handleSubscriptionShippingChange = null; + let handleShippingOptionsChange = null; + let handleShippingAddressChange = null; + let handleSubscriptionShippingOptionsChange = null; + let handleSubscriptionShippingAddressChange = null; if (shippingData.needsShipping && !config.finalReviewEnabled) { - handleShippingChange = async (data, actions) => { + handleShippingOptionsChange = async (data, actions) => { try { - const shippingOptionId = data.selected_shipping_option?.id; + const shippingOptionId = data.selectedShippingOption?.id; if (shippingOptionId) { + await wp.data.dispatch('wc/store/cart').selectShippingRate(shippingOptionId); await shippingData.setSelectedRates(shippingOptionId); } - const address = paypalAddressToWc(data.shipping_address); + const res = await fetch(config.ajax.update_shipping.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: config.ajax.update_shipping.nonce, + order_id: data.orderID, + }) + }); + + const json = await res.json(); + + if (!json.success) { + throw new Error(json.data.message); + } + } catch (e) { + console.error(e); + + actions.reject(); + } + }; + + handleShippingAddressChange = async (data, actions) => { + try { + const address = paypalAddressToWc(convertKeysToSnakeCase(data.shippingAddress)); await wp.data.dispatch('wc/store/cart').updateCustomerData({ shipping_address: address, @@ -325,14 +355,23 @@ const PayPalComponent = ({ } }; - handleSubscriptionShippingChange = async (data, actions) => { + handleSubscriptionShippingOptionsChange = async (data, actions) => { try { - const shippingOptionId = data.selected_shipping_option?.id; + const shippingOptionId = data.selectedShippingOption?.id; if (shippingOptionId) { + await wp.data.dispatch('wc/store/cart').selectShippingRate(shippingOptionId); await shippingData.setSelectedRates(shippingOptionId); } + } catch (e) { + console.error(e); - const address = paypalAddressToWc(data.shipping_address); + actions.reject(); + } + }; + + handleSubscriptionShippingAddressChange = async (data, actions) => { + try { + const address = paypalAddressToWc(convertKeysToSnakeCase(data.shippingAddress)); await wp.data.dispatch('wc/store/cart').updateCustomerData({ shipping_address: address, @@ -442,7 +481,8 @@ const PayPalComponent = ({ onError={onClose} createSubscription={createSubscription} onApprove={handleApproveSubscription} - onShippingChange={handleSubscriptionShippingChange} + onShippingOptionsChange={handleSubscriptionShippingOptionsChange} + onShippingAddressChange={handleSubscriptionShippingAddressChange} /> ); } @@ -456,7 +496,8 @@ const PayPalComponent = ({ onError={onClose} createOrder={createOrder} onApprove={handleApprove} - onShippingChange={handleShippingChange} + onShippingOptionsChange={handleShippingOptionsChange} + onShippingAddressChange={handleShippingAddressChange} /> ); } diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index f766cc6ed..d0e1a8543 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -137,7 +137,12 @@ const bootstrap = () => { } const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; - if (isFreeTrial && data.fundingSource !== 'card' && ! PayPalCommerceGateway.subscription_plan_id) { + if ( + isFreeTrial + && data.fundingSource !== 'card' + && ! PayPalCommerceGateway.subscription_plan_id + && ! PayPalCommerceGateway.vault_v3_enabled + ) { freeTrialHandler.handle(); return actions.reject(); } @@ -241,7 +246,6 @@ document.addEventListener( if (!typeof (PayPalCommerceGateway)) { console.error('PayPal button could not be configured.'); return; - return; } if ( diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index 93ecf7c8f..ed5926816 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -144,6 +144,54 @@ class CheckoutActionHandler { } } } + + addPaymentMethodConfiguration() { + return { + createVaultSetupToken: async () => { + const response = await fetch(this.config.ajax.create_setup_token.endpoint, { + method: "POST", + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: this.config.ajax.create_setup_token.nonce, + }) + }); + + const result = await response.json() + if (result.data.id) { + return result.data.id + } + + console.error(result) + }, + onApprove: async ({vaultSetupToken}) => { + const response = await fetch(this.config.ajax.create_payment_token_for_guest.endpoint, { + method: "POST", + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: this.config.ajax.create_payment_token_for_guest.nonce, + vault_setup_token: vaultSetupToken, + }) + }) + + const result = await response.json(); + if (result.success === true) { + document.querySelector('#place_order').click() + return; + } + + console.error(result) + }, + onError: (error) => { + console.error(error) + } + } + } } export default CheckoutActionHandler; diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index e7fae33b0..26a278736 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -116,6 +116,14 @@ class CheckoutBootstap { return; } + if( + PayPalCommerceGateway.is_free_trial_cart + && PayPalCommerceGateway.vault_v3_enabled + ) { + this.renderer.render(actionHandler.addPaymentMethodConfiguration(), {}, actionHandler.configuration()); + return; + } + this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration()); } diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index a812dc53e..6f781911b 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -145,6 +145,8 @@ return array( $container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.pay-now-contexts' ), $container->get( 'wcgateway.funding-sources-without-redirect' ), + $container->get( 'vaulting.vault-v3-enabled' ), + $container->get( 'api.endpoint.payment-tokens' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index d63b56af4..d795d4950 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use WC_Order; use WC_Product; use WC_Product_Variation; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; @@ -33,6 +34,9 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; +use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; +use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; +use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; @@ -184,13 +188,6 @@ class SmartButton implements SmartButtonInterface { */ private $funding_sources_without_redirect; - /** - * The logger. - * - * @var LoggerInterface - */ - private $logger; - /** * Session handler. * @@ -198,6 +195,27 @@ class SmartButton implements SmartButtonInterface { */ private $session_handler; + /** + * Whether Vault v3 module is enabled. + * + * @var bool + */ + private $vault_v3_enabled; + + /** + * Payment tokens endpoint. + * + * @var PaymentTokensEndpoint + */ + private $payment_tokens_endpoint; + + /** + * The logger. + * + * @var LoggerInterface + */ + private $logger; + /** * SmartButton constructor. * @@ -220,6 +238,8 @@ class SmartButton implements SmartButtonInterface { * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param array $pay_now_contexts The contexts that should have the Pay Now button. * @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. + * @param bool $vault_v3_enabled Whether Vault v3 module is enabled. + * @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -242,6 +262,8 @@ class SmartButton implements SmartButtonInterface { bool $early_validation_enabled, array $pay_now_contexts, array $funding_sources_without_redirect, + bool $vault_v3_enabled, + PaymentTokensEndpoint $payment_tokens_endpoint, LoggerInterface $logger ) { @@ -264,7 +286,9 @@ class SmartButton implements SmartButtonInterface { $this->early_validation_enabled = $early_validation_enabled; $this->pay_now_contexts = $pay_now_contexts; $this->funding_sources_without_redirect = $funding_sources_without_redirect; + $this->vault_v3_enabled = $vault_v3_enabled; $this->logger = $logger; + $this->payment_tokens_endpoint = $payment_tokens_endpoint; } /** @@ -631,7 +655,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ); - $has_paylater_block = has_block( 'woocommerce-paypal-payments/paylater-messages' ) && PayLaterBlockModule::is_enabled(); + $has_paylater_block = has_block( 'woocommerce-paypal-payments/paylater-messages' ) && PayLaterBlockModule::is_block_enabled( $this->settings_status ); switch ( $location ) { case 'checkout': @@ -878,7 +902,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages 'wrapper' => '#ppcp-messages', 'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ), 'block' => array( - 'enabled' => PayLaterBlockModule::is_enabled(), + 'enabled' => PayLaterBlockModule::is_block_enabled( $this->settings_status ), ), 'amount' => $amount, 'placement' => $placement, @@ -990,11 +1014,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 ); } /** @@ -1025,44 +1059,57 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages 'redirect' => wc_get_checkout_url(), 'context' => $this->context(), 'ajax' => array( - 'simulate_cart' => array( + 'simulate_cart' => array( 'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ), ), - 'change_cart' => array( + 'change_cart' => array( 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), - 'create_order' => array( + 'create_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), ), - 'approve_order' => array( + 'approve_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), ), - 'approve_subscription' => array( + 'approve_subscription' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ), ), - 'vault_paypal' => array( + 'vault_paypal' => array( 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), ), - 'save_checkout_form' => array( + 'save_checkout_form' => array( 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), ), - 'validate_checkout' => array( + 'validate_checkout' => array( 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), ), - 'cart_script_params' => array( + 'cart_script_params' => array( 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), ), + 'create_setup_token' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ), + 'nonce' => wp_create_nonce( CreateSetupToken::nonce() ), + ), + 'create_payment_token' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ), + 'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ), + ), + 'create_payment_token_for_guest' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentTokenForGuest::ENDPOINT ), + 'nonce' => wp_create_nonce( CreatePaymentTokenForGuest::nonce() ), + ), ), 'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(), 'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(), + 'vault_v3_enabled' => $this->vault_v3_enabled, 'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(), 'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(), 'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(), @@ -1318,7 +1365,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages $disable_funding, array_diff( array_keys( $this->all_funding_sources ), - array( 'venmo', 'paylater' ) + array( 'venmo', 'paylater', 'paypal' ) ) ); } @@ -1339,6 +1386,20 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages $disable_funding[] = 'paylater'; } + $disable_funding = array_filter( + $disable_funding, + /** + * Make sure paypal is not sent in disable funding. + * + * @param string $funding_source The funding_source. + * + * @psalm-suppress MissingClosureParamType + */ + function( $funding_source ) { + return $funding_source !== 'paypal'; + } + ); + if ( count( $disable_funding ) > 0 ) { $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); } @@ -1880,8 +1941,18 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages */ private function get_vaulted_paypal_email(): string { try { - $tokens = $this->get_payment_tokens(); + $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + if ( $customer_id ) { + $customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id ); + foreach ( $customer_tokens as $token ) { + $email_address = $token['payment_source']->properties()->email_address ?? ''; + if ( $email_address ) { + return $email_address; + } + } + } + $tokens = $this->get_payment_tokens(); foreach ( $tokens as $token ) { if ( isset( $token->source()->paypal ) ) { return $token->source()->paypal->payer->email_address; @@ -1890,6 +1961,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages } catch ( Exception $exception ) { $this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() ); } + return ''; } 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-order-tracking/src/Shipment/Shipment.php b/modules/ppcp-order-tracking/src/Shipment/Shipment.php index 099801874..9661d574c 100644 --- a/modules/ppcp-order-tracking/src/Shipment/Shipment.php +++ b/modules/ppcp-order-tracking/src/Shipment/Shipment.php @@ -169,10 +169,10 @@ class Shipment implements ShipmentInterface { $image = wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ); $ppcp_order_item = new Item( - mb_substr( $item->get_name(), 0, 127 ), + $this->prepare_item_string( $item->get_name() ), new Money( $price_without_tax_rounded, $currency ), $quantity, - $this->prepare_description( $product->get_description() ), + $this->prepare_item_string( $product->get_description() ), null, $this->prepare_sku( $product->get_sku() ), $product->is_virtual() ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, diff --git a/modules/ppcp-paylater-block/resources/js/edit.js b/modules/ppcp-paylater-block/resources/js/edit.js index 63330318c..44b4f895d 100644 --- a/modules/ppcp-paylater-block/resources/js/edit.js +++ b/modules/ppcp-paylater-block/resources/js/edit.js @@ -7,7 +7,7 @@ import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Help import PayPalMessages from "./components/PayPalMessages"; export default function Edit( { attributes, clientId, setAttributes } ) { - const { layout, logo, position, color, flexColor, flexRatio, placement, id } = attributes; + const { layout, logo, position, color, size, flexColor, flexRatio, placement, id } = attributes; const isFlex = layout === 'flex'; const [paypalScriptState, setPaypalScriptState] = useState(null); @@ -30,11 +30,12 @@ export default function Edit( { attributes, clientId, setAttributes } ) { ratio: flexRatio, text: { color, + size }, }; let classes = ['ppcp-paylater-block-preview', 'ppcp-overlay-parent']; - if (PcpPayLaterBlock.vaultingEnabled) { + if (PcpPayLaterBlock.vaultingEnabled || !PcpPayLaterBlock.placementEnabled) { classes = ['ppcp-paylater-block-preview', 'ppcp-paylater-unavailable', 'block-editor-warning']; } const props = useBlockProps({className: classes}); @@ -68,6 +69,27 @@ export default function Edit( { attributes, clientId, setAttributes } ) { } + if (!PcpPayLaterBlock.placementEnabled) { + return
+
+

{__('PayPal Pay Later Messaging', 'woocommerce-paypal-payments')}

+

{__('Pay Later Messaging cannot be used while the “WooCommerce Block” messaging placement is disabled. Enable the placement in the PayPal Payments Pay Later settings to reactivate this block.', 'woocommerce-paypal-payments')}

+
+ + + {__('PayPal Payments Settings', 'woocommerce-paypal-payments')} + + + + + +
+
+
+ } + let scriptParams = useScriptParams(PcpPayLaterBlock.ajax.cart_script_params); if (scriptParams === null) { return loadingElement; @@ -108,10 +130,10 @@ export default function Edit( { attributes, clientId, setAttributes } ) { { !isFlex && ( setAttributes({logo: value})} @@ -129,24 +151,31 @@ export default function Edit( { attributes, clientId, setAttributes } ) { { !isFlex && ( setAttributes({color: value})} />)} + { !isFlex && ( setAttributes({size: value})} + />)} { isFlex && ( setAttributes({flexColor: value})} @@ -154,8 +183,6 @@ export default function Edit( { attributes, clientId, setAttributes } ) { { isFlex && ( setAttributes( { placement: value } ) } diff --git a/modules/ppcp-paylater-block/resources/js/save.js b/modules/ppcp-paylater-block/resources/js/save.js index 0e221d29c..6a845e7da 100644 --- a/modules/ppcp-paylater-block/resources/js/save.js +++ b/modules/ppcp-paylater-block/resources/js/save.js @@ -1,7 +1,7 @@ import { useBlockProps } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { layout, logo, position, color, flexColor, flexRatio, placement, id } = attributes; + const { layout, logo, position, color, size, flexColor, flexRatio, placement, id } = attributes; const paypalAttributes = layout === 'flex' ? { 'data-pp-style-layout': 'flex', 'data-pp-style-color': flexColor, @@ -11,6 +11,7 @@ export default function save( { attributes } ) { 'data-pp-style-logo-type': logo, 'data-pp-style-logo-position': position, 'data-pp-style-text-color': color, + 'data-pp-style-text-size': size, }; if (placement && placement !== 'auto') { paypalAttributes['data-pp-placement'] = placement; diff --git a/modules/ppcp-paylater-block/src/PayLaterBlockModule.php b/modules/ppcp-paylater-block/src/PayLaterBlockModule.php index 9544c5686..223ccd972 100644 --- a/modules/ppcp-paylater-block/src/PayLaterBlockModule.php +++ b/modules/ppcp-paylater-block/src/PayLaterBlockModule.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; /** @@ -22,16 +23,26 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; */ class PayLaterBlockModule implements ModuleInterface { /** - * Returns whether the block should be loaded. + * Returns whether the block module should be loaded. */ - public static function is_enabled(): bool { + public static function is_module_loading_required(): bool { return apply_filters( // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_block_enabled', - getenv( 'PCP_PAYLATER_BLOCK' ) === '1' + getenv( 'PCP_PAYLATER_BLOCK' ) !== '0' ); } + /** + * Returns whether the block is enabled. + * + * @param SettingsStatus $settings_status The Settings status helper. + * @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( 'custom_placement' ); + } + /** * {@inheritDoc} */ @@ -71,13 +82,15 @@ class PayLaterBlockModule implements ModuleInterface { $script_handle, 'PcpPayLaterBlock', array( - 'ajax' => array( + 'ajax' => array( 'cart_script_params' => array( 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), ), ), - 'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ), - 'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ), + 'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ), + 'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ), + 'placementEnabled' => self::is_block_enabled( $c->get( 'wcgateway.settings.status' ) ), + 'payLaterSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=ppcp-pay-later' ), ) ); diff --git a/modules/ppcp-paylater-configurator/resources/css/paylater-configurator.scss b/modules/ppcp-paylater-configurator/resources/css/paylater-configurator.scss index 5d7cca405..6b61e8a2c 100644 --- a/modules/ppcp-paylater-configurator/resources/css/paylater-configurator.scss +++ b/modules/ppcp-paylater-configurator/resources/css/paylater-configurator.scss @@ -43,6 +43,11 @@ .css-1yo2lxy-text_body_strong, span.css-16jt5za-text_body, span.css-1yo2lxy-text_body_strong, span { font-size: 14px; } + + hr { + margin-right: 16px; + border-top-color: #B1B7BD; + } } #field-pay_later_messaging_heading h3{ diff --git a/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js b/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js index 7250e8ce0..9549ef2af 100644 --- a/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js +++ b/modules/ppcp-paylater-configurator/resources/js/paylater-configurator.js @@ -15,13 +15,22 @@ document.addEventListener( 'DOMContentLoaded', () => { headingRow.parentNode.insertBefore(newRow, headingRow.nextSibling); - saveChangesButton.addEventListener('click', () => { - form.querySelector('.' + publishButtonClassName).click(); + let isSaving = false; // Flag variable to track whether saving is in progress - // Delay the page refresh by a few milliseconds to ensure changes take effect - setTimeout(() => { - location.reload(); - }, 1000); + saveChangesButton.addEventListener('click', () => { + // Check if saving is not already in progress + if (!isSaving) { + isSaving = true; // Set flag to indicate saving is in progress + + // Trigger the click event on the publish button + form.querySelector('.' + publishButtonClassName).click(); + + // Trigger click event on saveChangesButton after a short delay + setTimeout(() => { + saveChangesButton.click(); // Trigger click event on saveChangesButton + isSaving = false; // Reset flag when saving is complete + }, 1000); // Adjust the delay as needed + } }); merchantConfigurators.Messaging({ @@ -30,7 +39,7 @@ document.addEventListener( 'DOMContentLoaded', () => { partnerClientId: PcpPayLaterConfigurator.partnerClientId, partnerName: 'WooCommerce', bnCode: 'Woo_PPCP', - placements: ['cart', 'checkout', 'product', 'category', 'homepage', 'custom_placement'], + 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 051a89dd6..df4e4affa 100644 --- a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php +++ b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php @@ -99,14 +99,15 @@ class SaveConfig { $this->settings->set( 'pay_later_messaging_enabled', true ); $enabled_locations = array(); - foreach ( $config as $placement => $data ) { - $location = $this->configurator_placement_to_location( $placement ); + $this->save_config_for_location( $data, $placement ); - $this->save_config_for_location( $data, $location ); + if ( $placement === 'custom_placement' ) { + $data = $data[0] ?? array(); + } if ( $data['status'] === 'enabled' ) { - $enabled_locations[] = $location; + $enabled_locations[] = $placement; } } @@ -131,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" ); } /** @@ -145,24 +147,4 @@ class SaveConfig { $this->settings->set( $settings_key, $config[ $key ] ); } } - - /** - * Converts the configurator placement into location in the old settings. - * - * @param string $placement The configurator placement. - */ - private function configurator_placement_to_location( string $placement ): string { - switch ( $placement ) { - case 'cart': - case 'checkout': - case 'product': - return $placement; - case 'category': - return 'shop'; - case 'homepage': - return 'home'; - default: - return ''; - } - } } diff --git a/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php b/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php index cb272984d..c73bbd0ab 100644 --- a/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php +++ b/modules/ppcp-paylater-configurator/src/Factory/ConfigFactory.php @@ -22,11 +22,12 @@ class ConfigFactory { */ public function from_settings( Settings $settings ): array { return array( - $this->location_to_configurator_placement( 'cart' ) => $this->for_location( $settings, 'cart' ), - $this->location_to_configurator_placement( 'checkout' ) => $this->for_location( $settings, 'checkout' ), - $this->location_to_configurator_placement( 'product' ) => $this->for_location( $settings, 'product' ), - $this->location_to_configurator_placement( 'shop' ) => $this->for_location( $settings, 'shop' ), - $this->location_to_configurator_placement( 'home' ) => $this->for_location( $settings, 'home' ), + 'cart' => $this->for_location( $settings, 'cart' ), + 'checkout' => $this->for_location( $settings, 'checkout' ), + 'product' => $this->for_location( $settings, 'product' ), + 'shop' => $this->for_location( $settings, 'shop' ), + 'home' => $this->for_location( $settings, 'home' ), + 'custom_placement' => array( $this->for_location( $settings, 'woocommerceBlock' ) ), ); } @@ -39,51 +40,88 @@ 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(); - $placement = $this->location_to_configurator_placement( $location ); - if ( in_array( $placement, array( 'category', 'homepage' ), true ) ) { - $config = array( - 'layout' => 'flex', - 'color' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_color", 'black', array( 'black', 'blue', 'white', 'white-no-border' ) ), - 'ratio' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_ratio", '8x1', array( '8x1', '20x1' ) ), - ); - } else { - $config = array( - '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' => $placement, - ), - $config + 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, ); } /** - * Converts the location name from the old settings into the configurator placement. + * Returns the configurator config for woocommerceBlock location. * - * @param string $location The location name in the old settings. + * @param array $selected_locations The list of selected locations. + * @return array{ + * status: "disabled"|"enabled", + * message_reference: string + * } The configurator config map. */ - private function location_to_configurator_placement( string $location ): string { - switch ( $location ) { - case 'cart': - case 'checkout': - case 'product': - return $location; - case 'shop': - return 'category'; - case 'home': - return 'homepage'; - default: - return ''; - } + 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, + ); } /** @@ -93,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-paylater-configurator/src/PayLaterConfiguratorModule.php b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php index d3e110289..fd5d4c6d5 100644 --- a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php +++ b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php @@ -29,7 +29,7 @@ class PayLaterConfiguratorModule implements ModuleInterface { return apply_filters( // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_configurator_enabled', - getenv( 'PCP_PAYLATER_CONFIGURATOR' ) === '1' + getenv( 'PCP_PAYLATER_CONFIGURATOR' ) !== '0' ); } @@ -79,7 +79,7 @@ class PayLaterConfiguratorModule implements ModuleInterface { static function () use ( $c, $settings ) { wp_enqueue_script( 'ppcp-paylater-configurator-lib', - 'https://www.paypalobjects.com/merchant-library/preview/merchant-configurator.js', + 'https://www.paypalobjects.com/merchant-library/merchant-configurator.js', array(), $c->get( 'ppcp.asset-version' ), true diff --git a/modules/ppcp-paypal-subscriptions/src/SubscriptionsApiHandler.php b/modules/ppcp-paypal-subscriptions/src/SubscriptionsApiHandler.php index 4d56558ab..2109f13d2 100644 --- a/modules/ppcp-paypal-subscriptions/src/SubscriptionsApiHandler.php +++ b/modules/ppcp-paypal-subscriptions/src/SubscriptionsApiHandler.php @@ -114,7 +114,7 @@ class SubscriptionsApiHandler { */ public function create_product( WC_Product $product ) { try { - $subscription_product = $this->products_endpoint->create( $product->get_title(), $this->prepare_description( $product->get_description() ) ); + $subscription_product = $this->products_endpoint->create( $this->prepare_item_string( $product->get_title() ), $this->prepare_item_string( $product->get_description() ) ); $product->update_meta_data( 'ppcp_subscription_product', $subscription_product->to_array() ); $product->save(); } catch ( RuntimeException $exception ) { @@ -169,7 +169,7 @@ class SubscriptionsApiHandler { $catalog_product_name = $catalog_product->name() ?: ''; $catalog_product_description = $catalog_product->description() ?: ''; - $wc_product_description = $this->prepare_description( $product->get_description() ) ?: $product->get_title(); + $wc_product_description = $this->prepare_item_string( $product->get_description() ) ?: $this->prepare_item_string( $product->get_title() ); if ( $catalog_product_name !== $product->get_title() || $catalog_product_description !== $wc_product_description ) { $data = array(); diff --git a/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js b/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js index 8aca64f7c..6178bd92c 100644 --- a/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js +++ b/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js @@ -12,7 +12,7 @@ import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper"; const errorHandler = new ErrorHandler( - PayPalCommerceGateway.labels.error.generic, + ppcp_add_payment_method.labels.error.generic, document.querySelector('.woocommerce-notices-wrapper') ); diff --git a/modules/ppcp-save-payment-methods/services.php b/modules/ppcp-save-payment-methods/services.php index 5d04b16ab..a0b43f021 100644 --- a/modules/ppcp-save-payment-methods/services.php +++ b/modules/ppcp-save-payment-methods/services.php @@ -9,9 +9,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\SavePaymentMethods; -use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; +use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest; use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -805,31 +805,17 @@ return array( $container->get( 'api.endpoint.payment-method-tokens' ) ); }, - 'save-payment-methods.wc-payment-tokens' => static function( ContainerInterface $container ): WooCommercePaymentTokens { - return new WooCommercePaymentTokens( - $container->get( 'vaulting.payment-token-helper' ), - $container->get( 'vaulting.payment-token-factory' ), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, 'save-payment-methods.endpoint.create-payment-token' => static function ( ContainerInterface $container ): CreatePaymentToken { return new CreatePaymentToken( $container->get( 'button.request-data' ), $container->get( 'api.endpoint.payment-method-tokens' ), - $container->get( 'save-payment-methods.wc-payment-tokens' ) + $container->get( 'vaulting.wc-payment-tokens' ) ); }, - 'save-payment-methods.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment { - return new CaptureCardPayment( - $container->get( 'api.host' ), - $container->get( 'api.bearer' ), - $container->get( 'api.factory.order' ), - $container->get( 'api.factory.purchase-unit' ), - $container->get( 'api.endpoint.order' ), - $container->get( 'session.handler' ), - $container->get( 'wc-subscriptions.helpers.real-time-account-updater' ), - $container->get( 'wcgateway.settings' ), - $container->get( 'woocommerce.logger.woocommerce' ) + 'save-payment-methods.endpoint.create-payment-token-for-guest' => static function ( ContainerInterface $container ): CreatePaymentTokenForGuest { + return new CreatePaymentTokenForGuest( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.payment-method-tokens' ) ); }, ); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index 3a4807ec2..4fea1f188 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php @@ -14,9 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; -use WooCommerce\PayPalCommerce\SavePaymentMethods\WooCommercePaymentTokens; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; /** * Class CreatePaymentToken @@ -96,7 +94,7 @@ class CreatePaymentToken implements EndpointInterface { ) ); - $result = $this->payment_method_tokens_endpoint->payment_tokens( $payment_source ); + $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source ); if ( is_user_logged_in() && isset( $result->customer->id ) ) { $current_user_id = get_current_user_id(); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentTokenForGuest.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentTokenForGuest.php new file mode 100644 index 000000000..e458c96be --- /dev/null +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentTokenForGuest.php @@ -0,0 +1,90 @@ +request_data = $request_data; + $this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint; + } + + /** + * Returns the nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws Exception On Error. + */ + public function handle_request(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + + /** + * Suppress ArgumentTypeCoercion + * + * @psalm-suppress ArgumentTypeCoercion + */ + $payment_source = new PaymentSource( + 'token', + (object) array( + 'id' => $data['vault_setup_token'], + 'type' => 'SETUP_TOKEN', + ) + ); + + $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source ); + WC()->session->set( 'ppcp_guest_payment_for_free_trial', $result ); + + wp_send_json_success(); + return true; + } +} diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 7bfb52cee..b667fb10b 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -19,9 +19,10 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; -use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; +use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; @@ -141,7 +142,7 @@ class SavePaymentMethodsModule implements ModuleInterface { 'vault' => array( 'store_in_vault' => 'ON_SUCCESS', 'usage_type' => 'MERCHANT', - 'permit_multiple_payment_tokens' => true, + 'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ), ), ), ), @@ -167,7 +168,7 @@ class SavePaymentMethodsModule implements ModuleInterface { 'vault' => array( 'store_in_vault' => 'ON_SUCCESS', 'usage_type' => 'MERCHANT', - 'permit_multiple_payment_tokens' => true, + 'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ), ), ), ), @@ -197,7 +198,7 @@ class SavePaymentMethodsModule implements ModuleInterface { update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id ); - $wc_payment_tokens = $c->get( 'save-payment-methods.wc-payment-tokens' ); + $wc_payment_tokens = $c->get( 'vaulting.wc-payment-tokens' ); assert( $wc_payment_tokens instanceof WooCommercePaymentTokens ); if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { @@ -281,7 +282,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 @@ -312,6 +317,14 @@ class SavePaymentMethodsModule implements ModuleInterface { 'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ), ), ), + 'labels' => array( + 'error' => array( + 'generic' => __( + 'Something went wrong. Please try again or choose another payment source.', + 'woocommerce-paypal-payments' + ), + ), + ), ) ); } catch ( RuntimeException $exception ) { @@ -359,6 +372,16 @@ class SavePaymentMethodsModule implements ModuleInterface { } ); + add_action( + 'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' ); + assert( $endpoint instanceof CreatePaymentTokenForGuest ); + + $endpoint->handle_request(); + } + ); + add_action( 'woocommerce_paypal_payments_before_delete_payment_token', function( string $token_id ) use ( $c ) { diff --git a/modules/ppcp-vaulting/services.php b/modules/ppcp-vaulting/services.php index 44d042a7e..45355225d 100644 --- a/modules/ppcp-vaulting/services.php +++ b/modules/ppcp-vaulting/services.php @@ -56,4 +56,15 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'vaulting.wc-payment-tokens' => static function( ContainerInterface $container ): WooCommercePaymentTokens { + return new WooCommercePaymentTokens( + $container->get( 'vaulting.payment-token-helper' ), + $container->get( 'vaulting.payment-token-factory' ), + $container->get( 'api.endpoint.payment-tokens' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'vaulting.vault-v3-enabled' => static function( ContainerInterface $container ): bool { + return $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' ); + }, ); diff --git a/modules/ppcp-save-payment-methods/src/WooCommercePaymentTokens.php b/modules/ppcp-vaulting/src/WooCommercePaymentTokens.php similarity index 70% rename from modules/ppcp-save-payment-methods/src/WooCommercePaymentTokens.php rename to modules/ppcp-vaulting/src/WooCommercePaymentTokens.php index b0f37a449..9136f7d85 100644 --- a/modules/ppcp-save-payment-methods/src/WooCommercePaymentTokens.php +++ b/modules/ppcp-vaulting/src/WooCommercePaymentTokens.php @@ -7,18 +7,15 @@ declare(strict_types=1); -namespace WooCommerce\PayPalCommerce\SavePaymentMethods; +namespace WooCommerce\PayPalCommerce\Vaulting; use Exception; use Psr\Log\LoggerInterface; use stdClass; use WC_Payment_Token_CC; use WC_Payment_Tokens; -use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenApplePay; -use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenFactory; -use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenHelper; -use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenPayPal; -use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenVenmo; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -41,6 +38,13 @@ class WooCommercePaymentTokens { */ private $payment_token_factory; + /** + * Payment tokens endpoint. + * + * @var PaymentTokensEndpoint + */ + private $payment_tokens_endpoint; + /** * The logger. * @@ -51,18 +55,21 @@ class WooCommercePaymentTokens { /** * WooCommercePaymentTokens constructor. * - * @param PaymentTokenHelper $payment_token_helper The payment token helper. - * @param PaymentTokenFactory $payment_token_factory The payment token factory. - * @param LoggerInterface $logger The logger. + * @param PaymentTokenHelper $payment_token_helper The payment token helper. + * @param PaymentTokenFactory $payment_token_factory The payment token factory. + * @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint. + * @param LoggerInterface $logger The logger. */ public function __construct( PaymentTokenHelper $payment_token_helper, PaymentTokenFactory $payment_token_factory, + PaymentTokensEndpoint $payment_tokens_endpoint, LoggerInterface $logger ) { - $this->payment_token_helper = $payment_token_helper; - $this->payment_token_factory = $payment_token_factory; - $this->logger = $logger; + $this->payment_token_helper = $payment_token_helper; + $this->payment_token_factory = $payment_token_factory; + $this->payment_tokens_endpoint = $payment_tokens_endpoint; + $this->logger = $logger; } /** @@ -80,6 +87,10 @@ class WooCommercePaymentTokens { string $email ): int { + if ( $customer_id === 0 ) { + return 0; + } + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID ); if ( $this->payment_token_helper->token_exist( $wc_tokens, $token, PaymentTokenPayPal::class ) ) { return 0; @@ -128,6 +139,10 @@ class WooCommercePaymentTokens { string $email ): int { + if ( $customer_id === 0 ) { + return 0; + } + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID ); if ( $this->payment_token_helper->token_exist( $wc_tokens, $token, PaymentTokenVenmo::class ) ) { return 0; @@ -174,6 +189,10 @@ class WooCommercePaymentTokens { string $token ): int { + if ( $customer_id === 0 ) { + return 0; + } + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID ); if ( $this->payment_token_helper->token_exist( $wc_tokens, $token, PaymentTokenApplePay::class ) ) { return 0; @@ -212,6 +231,10 @@ class WooCommercePaymentTokens { * @return int */ public function create_payment_token_card( int $customer_id, stdClass $payment_token ): int { + if ( $customer_id === 0 ) { + return 0; + } + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, CreditCardGateway::ID ); if ( $this->payment_token_helper->token_exist( $wc_tokens, $payment_token->id ) ) { return 0; @@ -219,7 +242,7 @@ class WooCommercePaymentTokens { $token = new WC_Payment_Token_CC(); $token->set_token( $payment_token->id ); - $token->set_user_id( get_current_user_id() ); + $token->set_user_id( $customer_id ); $token->set_gateway_id( CreditCardGateway::ID ); $token->set_last4( $payment_token->payment_source->card->last_digits ?? '' ); @@ -243,4 +266,61 @@ class WooCommercePaymentTokens { $token->save(); return $token->get_id(); } + + /** + * Returns PayPal payment tokens for the given WP user id. + * + * @param int $user_id WP user id. + * @return array + */ + public function customer_tokens( int $user_id ): array { + $customer_id = get_user_meta( $user_id, '_ppcp_target_customer_id', true ); + if ( ! $customer_id ) { + $customer_id = get_user_meta( $user_id, 'ppcp_customer_id', true ); + } + + try { + $customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id ); + } catch ( RuntimeException $exception ) { + $customer_tokens = array(); + } + + return $customer_tokens; + } + + /** + * Creates WC payment tokens for the given WP user id using PayPal payment tokens as source. + * + * @param array $customer_tokens PayPal customer payment tokens. + * @param int $user_id WP user id. + * @return void + */ + public function create_wc_tokens( array $customer_tokens, int $user_id ): void { + foreach ( $customer_tokens as $customer_token ) { + if ( $customer_token['payment_source']->name() === 'paypal' ) { + $this->create_payment_token_paypal( + $user_id, + $customer_token['id'], + $customer_token['payment_source']->properties()->email_address ?? '' + ); + } + + if ( $customer_token['payment_source']->name() === 'card' ) { + /** + * Suppress ArgumentTypeCoercion + * + * @psalm-suppress ArgumentTypeCoercion + */ + $this->create_payment_token_card( + $user_id, + (object) array( + 'id' => $customer_token['id'], + 'payment_source' => (object) array( + $customer_token['payment_source']->name() => $customer_token['payment_source']->properties(), + ), + ) + ); + } + } + } } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 3f20d12dd..6bf3d5ecf 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -22,6 +22,8 @@ use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\State; +use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -101,7 +103,10 @@ return array( $api_shop_country, $container->get( 'api.endpoint.order' ), $container->get( 'api.factory.paypal-checkout-url' ), - $container->get( 'wcgateway.place-order-button-text' ) + $container->get( 'wcgateway.place-order-button-text' ), + $container->get( 'api.endpoint.payment-tokens' ), + $container->get( 'vaulting.vault-v3-enabled' ), + $container->get( 'vaulting.wc-payment-tokens' ) ); }, 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { @@ -131,8 +136,10 @@ return array( $vaulted_credit_card_handler, $container->get( 'onboarding.environment' ), $container->get( 'api.endpoint.order' ), - $container->get( 'save-payment-methods.endpoint.capture-card-payment' ), + $container->get( 'wcgateway.endpoint.capture-card-payment' ), $container->get( 'api.prefix' ), + $container->get( 'api.endpoint.payment-tokens' ), + $container->get( 'vaulting.wc-payment-tokens' ), $logger ); }, @@ -384,19 +391,25 @@ return array( $notice = $container->get( 'wcgateway.notice.authorize-order-action' ); $settings = $container->get( 'wcgateway.settings' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' ); + $amount_factory = $container->get( 'api.factory.amount' ); return new AuthorizedPaymentsProcessor( $order_endpoint, $payments_endpoint, $logger, $notice, $settings, - $subscription_helper + $subscription_helper, + $amount_factory ); }, 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new RenderAuthorizeAction( $column ); }, + 'wcgateway.admin.render-reauthorize-action' => static function ( ContainerInterface $container ): RenderReauthorizeAction { + $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); + return new RenderReauthorizeAction( $column ); + }, 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new PaymentStatusOrderDetail( $column ); @@ -952,10 +965,10 @@ return array( 'ideal' => _x( 'iDEAL', 'Name of payment method', 'woocommerce-paypal-payments' ), 'mybank' => _x( 'MyBank', 'Name of payment method', 'woocommerce-paypal-payments' ), 'p24' => _x( 'Przelewy24', 'Name of payment method', 'woocommerce-paypal-payments' ), - 'sofort' => _x( 'Sofort', 'Name of payment method', 'woocommerce-paypal-payments' ), 'venmo' => _x( 'Venmo', 'Name of payment method', 'woocommerce-paypal-payments' ), 'trustly' => _x( 'Trustly', 'Name of payment method', 'woocommerce-paypal-payments' ), - 'paylater' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), + 'paylater' => _x( 'PayPal Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), + 'paypal' => _x( 'PayPal', 'Name of payment method', 'woocommerce-paypal-payments' ), ); }, @@ -978,6 +991,7 @@ return array( array_flip( array( 'paylater', + 'paypal', ) ) ); @@ -1579,4 +1593,17 @@ return array( ) ); }, + 'wcgateway.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment { + return new CaptureCardPayment( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'api.factory.order' ), + $container->get( 'api.factory.purchase-unit' ), + $container->get( 'api.endpoint.order' ), + $container->get( 'session.handler' ), + $container->get( 'wc-subscriptions.helpers.real-time-account-updater' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, ); diff --git a/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php b/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php index 1d38e64a6..4185f6125 100644 --- a/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php +++ b/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php @@ -9,9 +9,6 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Admin; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; -use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; - /** * Class RenderAuthorizeAction */ diff --git a/modules/ppcp-wc-gateway/src/Admin/RenderReauthorizeAction.php b/modules/ppcp-wc-gateway/src/Admin/RenderReauthorizeAction.php new file mode 100644 index 000000000..71dbed0ff --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Admin/RenderReauthorizeAction.php @@ -0,0 +1,67 @@ +column = $column; + } + + /** + * Renders the action into the $order_actions array based on the WooCommerce order. + * + * @param array $order_actions The actions to render into. + * @param \WC_Order $wc_order The order for which to render the action. + * + * @return array + */ + public function render( array $order_actions, \WC_Order $wc_order ) : array { + + if ( ! $this->should_render_for_order( $wc_order ) ) { + return $order_actions; + } + + $order_actions['ppcp_reauthorize_order'] = esc_html__( + 'Reauthorize PayPal payment', + 'woocommerce-paypal-payments' + ); + return $order_actions; + } + + /** + * Whether the action should be rendered for a certain WooCommerce order. + * + * @param \WC_Order $order The Woocommerce order. + * + * @return bool + */ + private function should_render_for_order( \WC_Order $order ) : bool { + $status = $order->get_status(); + $not_allowed_statuses = array( 'refunded', 'cancelled', 'failed' ); + return $this->column->should_render_for_order( $order ) && + ! $this->column->is_captured( $order ) && + ! in_array( $status, $not_allowed_statuses, true ); + } +} diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CaptureCardPayment.php b/modules/ppcp-wc-gateway/src/Endpoint/CaptureCardPayment.php similarity index 98% rename from modules/ppcp-save-payment-methods/src/Endpoint/CaptureCardPayment.php rename to modules/ppcp-wc-gateway/src/Endpoint/CaptureCardPayment.php index 73df58a70..bfb8b4bc4 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CaptureCardPayment.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/CaptureCardPayment.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint; +namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint; use Psr\Log\LoggerInterface; use RuntimeException; diff --git a/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php b/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php index 071410615..cdfcb3b47 100644 --- a/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php +++ b/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php @@ -34,7 +34,7 @@ class FundingSourceRenderer { * * @var string[] */ - protected $own_funding_sources = array( 'venmo', 'paylater' ); + protected $own_funding_sources = array( 'venmo', 'paylater', 'paypal' ); /** * FundingSourceRenderer constructor. @@ -63,7 +63,7 @@ class FundingSourceRenderer { return $this->funding_sources[ $id ]; } return sprintf( - /* translators: %s - Sofort, BLIK, iDeal, Mercado Pago, etc. */ + /* translators: %s - BLIK, iDeal, Mercado Pago, etc. */ __( '%s (via PayPal)', 'woocommerce-paypal-payments' ), $this->funding_sources[ $id ] ); @@ -84,7 +84,7 @@ class FundingSourceRenderer { if ( array_key_exists( $id, $this->funding_sources ) ) { return sprintf( - /* translators: %s - Sofort, BLIK, iDeal, Mercado Pago, etc. */ + /* translators: %s - BLIK, iDeal, Mercado Pago, etc. */ __( 'Pay via %s.', 'woocommerce-paypal-payments' ), $this->funding_sources[ $id ] ); diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index f2903d239..6e21e70df 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -15,30 +15,33 @@ use WC_Order; use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; -use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment; use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; -use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; -use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler; -use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; -use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; -use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; -use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; +use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; +use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; +use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; /** * Class CreditCardGateway */ class CreditCardGateway extends \WC_Payment_Gateway_CC { - use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait; + use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait, FreeTrialHandlerTrait; const ID = 'ppcp-credit-card-gateway'; @@ -154,6 +157,20 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ private $prefix; + /** + * Payment tokens endpoint. + * + * @var PaymentTokensEndpoint + */ + private $payment_tokens_endpoint; + + /** + * WooCommerce payment tokens factory. + * + * @var WooCommercePaymentTokens + */ + private $wc_payment_tokens; + /** * The logger. * @@ -179,6 +196,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { * @param OrderEndpoint $order_endpoint The order endpoint. * @param CaptureCardPayment $capture_card_payment Capture card payment. * @param string $prefix The prefix. + * @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint. + * @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens factory. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -197,6 +216,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { OrderEndpoint $order_endpoint, CaptureCardPayment $capture_card_payment, string $prefix, + PaymentTokensEndpoint $payment_tokens_endpoint, + WooCommercePaymentTokens $wc_payment_tokens, LoggerInterface $logger ) { $this->id = self::ID; @@ -215,6 +236,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { $this->order_endpoint = $order_endpoint; $this->capture_card_payment = $capture_card_payment; $this->prefix = $prefix; + $this->payment_tokens_endpoint = $payment_tokens_endpoint; + $this->wc_payment_tokens = $wc_payment_tokens; $this->logger = $logger; if ( $state->current_state() === State::STATE_ONBOARDED ) { @@ -291,8 +314,10 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ public function form() { add_action( 'gettext', array( $this, 'replace_credit_card_cvv_label' ), 10, 3 ); + add_action( 'gettext', array( $this, 'replace_credit_card_cvv_placeholder' ), 10, 3 ); parent::form(); remove_action( 'gettext', 'replace_credit_card_cvv_label' ); + remove_action( 'gettext', 'replace_credit_card_cvv_placeholder' ); } /** @@ -312,6 +337,23 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { return __( 'CVV', 'woocommerce-paypal-payments' ); } + /** + * Replace WooCommerce credit card CVV field placeholder. + * + * @param string $translation Translated text. + * @param string $text Original text to translate. + * @param string $domain Text domain. + * + * @return string Translated field. + */ + public function replace_credit_card_cvv_placeholder( string $translation, string $text, string $domain ): string { + if ( 'woocommerce' !== $domain || 'CVC' !== $text || ! apply_filters( 'woocommerce_paypal_payments_card_fields_translate_card_cvv', true ) ) { + return $translation; + } + + return __( 'CVV', 'woocommerce-paypal-payments' ); + } + /** * Returns the icons of the gateway. * @@ -413,10 +455,39 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { // phpcs:ignore WordPress.Security.NonceVerification.Missing $card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) ); + + if ( $this->is_free_trial_order( $wc_order ) && $card_payment_token_id ) { + $customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() ); + foreach ( $customer_tokens as $token ) { + if ( $token['payment_source']->name() === 'card' ) { + $wc_order->payment_complete(); + return $this->handle_payment_success( $wc_order ); + } + } + } + if ( $card_payment_token_id ) { + $customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() ); + + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), self::ID ); + + if ( $customer_tokens && empty( $wc_tokens ) ) { + $this->wc_payment_tokens->create_wc_tokens( $customer_tokens, get_current_user_id() ); + } + + $customer_token_ids = array(); + foreach ( $customer_tokens as $customer_token ) { + $customer_token_ids[] = $customer_token['id']; + } + $tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() ); foreach ( $tokens as $token ) { if ( $token->get_id() === (int) $card_payment_token_id ) { + if ( ! in_array( $token->get_token(), $customer_token_ids, true ) ) { + $token->delete(); + continue; + } + $custom_id = $wc_order->get_order_number(); $invoice_id = $this->prefix . $wc_order->get_order_number(); $create_order = $this->capture_card_payment->create_order( $token->get_token(), $custom_id, $invoice_id ); diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 5c3418d86..6c6568b8d 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -14,12 +14,14 @@ use Psr\Log\LoggerInterface; use WC_Order; use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; @@ -48,12 +50,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. * @@ -173,26 +181,50 @@ class PayPalGateway extends \WC_Payment_Gateway { */ private $paypal_checkout_url_factory; + /** + * Payment tokens endpoint. + * + * @var PaymentTokensEndpoint + */ + private $payment_tokens_endpoint; + + /** + * Whether Vault v3 module is enabled. + * + * @var bool + */ + private $vault_v3_enabled; + + /** + * WooCommerce payment tokens. + * + * @var WooCommercePaymentTokens + */ + private $wc_payment_tokens; + /** * PayPalGateway constructor. * - * @param SettingsRenderer $settings_renderer The Settings Renderer. - * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. - * @param OrderProcessor $order_processor The Order Processor. - * @param ContainerInterface $config The settings. - * @param SessionHandler $session_handler The Session Handler. - * @param RefundProcessor $refund_processor The Refund Processor. - * @param State $state The state. - * @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order. - * @param SubscriptionHelper $subscription_helper The subscription helper. - * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. - * @param Environment $environment The environment. - * @param PaymentTokenRepository $payment_token_repository The payment token repository. - * @param LoggerInterface $logger The logger. - * @param string $api_shop_country The api shop country. - * @param OrderEndpoint $order_endpoint The order endpoint. - * @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID. - * @param string $place_order_button_text The text for the standard "Place order" button. + * @param SettingsRenderer $settings_renderer The Settings Renderer. + * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. + * @param OrderProcessor $order_processor The Order Processor. + * @param ContainerInterface $config The settings. + * @param SessionHandler $session_handler The Session Handler. + * @param RefundProcessor $refund_processor The Refund Processor. + * @param State $state The state. + * @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order. + * @param SubscriptionHelper $subscription_helper The subscription helper. + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. + * @param Environment $environment The environment. + * @param PaymentTokenRepository $payment_token_repository The payment token repository. + * @param LoggerInterface $logger The logger. + * @param string $api_shop_country The api shop country. + * @param OrderEndpoint $order_endpoint The order endpoint. + * @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID. + * @param string $place_order_button_text The text for the standard "Place order" button. + * @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint. + * @param bool $vault_v3_enabled Whether Vault v3 module is enabled. + * @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens. */ public function __construct( SettingsRenderer $settings_renderer, @@ -211,7 +243,10 @@ class PayPalGateway extends \WC_Payment_Gateway { string $api_shop_country, OrderEndpoint $order_endpoint, callable $paypal_checkout_url_factory, - string $place_order_button_text + string $place_order_button_text, + PaymentTokensEndpoint $payment_tokens_endpoint, + bool $vault_v3_enabled, + WooCommercePaymentTokens $wc_payment_tokens ) { $this->id = self::ID; $this->settings_renderer = $settings_renderer; @@ -231,6 +266,10 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->api_shop_country = $api_shop_country; $this->paypal_checkout_url_factory = $paypal_checkout_url_factory; $this->order_button_text = $place_order_button_text; + $this->order_endpoint = $order_endpoint; + $this->payment_tokens_endpoint = $payment_tokens_endpoint; + $this->vault_v3_enabled = $vault_v3_enabled; + $this->wc_payment_tokens = $wc_payment_tokens; if ( $this->onboarded ) { $this->supports = array( 'refunds', 'tokenization' ); @@ -295,8 +334,6 @@ class PayPalGateway extends \WC_Payment_Gateway { 'process_admin_options', ) ); - - $this->order_endpoint = $order_endpoint; } /** @@ -496,7 +533,49 @@ class PayPalGateway extends \WC_Payment_Gateway { $wc_order->save(); } - if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) && ! $this->subscription_helper->paypal_subscription_id() ) { + if ( + 'card' !== $funding_source + && $this->is_free_trial_order( $wc_order ) + && ! $this->subscription_helper->paypal_subscription_id() + ) { + $ppcp_guest_payment_for_free_trial = WC()->session->get( 'ppcp_guest_payment_for_free_trial' ) ?? null; + if ( $this->vault_v3_enabled && $ppcp_guest_payment_for_free_trial ) { + $customer_id = $ppcp_guest_payment_for_free_trial->customer->id ?? ''; + if ( $customer_id ) { + update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id ); + } + + if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal ) ) { + $email = ''; + if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address ) ) { + $email = $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address; + } + + $this->wc_payment_tokens->create_payment_token_paypal( + $wc_order->get_customer_id(), + $ppcp_guest_payment_for_free_trial->id, + $email + ); + } + + WC()->session->set( 'ppcp_guest_payment_for_free_trial', null ); + + $wc_order->payment_complete(); + return $this->handle_payment_success( $wc_order ); + } + + $customer_id = get_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', true ); + if ( $customer_id ) { + $customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id ); + foreach ( $customer_tokens as $token ) { + $payment_source_name = $token['payment_source']->name() ?? ''; + if ( $payment_source_name === 'paypal' || $payment_source_name === 'venmo' ) { + $wc_order->payment_complete(); + return $this->handle_payment_success( $wc_order ); + } + } + } + $user_id = (int) $wc_order->get_customer_id(); $tokens = $this->payment_token_repository->all_for_user_id( $user_id ); if ( ! array_filter( @@ -509,7 +588,6 @@ class PayPalGateway extends \WC_Payment_Gateway { } $wc_order->payment_complete(); - return $this->handle_payment_success( $wc_order ); } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php index 20ab81fb6..d4ca29121 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php @@ -198,25 +198,41 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway { 'description' => __( "Specify brand name, logo and customer service instructions to be presented on Ratepay's payment instructions.", 'woocommerce-paypal-payments' ), ), 'brand_name' => array( - 'title' => __( 'Brand name', 'woocommerce-paypal-payments' ), - 'type' => 'text', - 'default' => get_bloginfo( 'name' ) ?? '', - 'desc_tip' => true, - 'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + 'title' => __( 'Brand name', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => get_bloginfo( 'name' ) ?? '', + 'desc_tip' => true, + 'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions. Should not exceed 127 characters.', 'woocommerce-paypal-payments' ), + 'maxlength' => 127, + 'custom_attributes' => array( + 'pattern' => '.{1,127}', + 'autocomplete' => 'off', + 'required' => '', + ), ), 'logo_url' => array( - 'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ), - 'type' => 'url', - 'default' => '', - 'desc_tip' => true, - 'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + 'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ), + 'type' => 'url', + 'default' => '', + 'desc_tip' => true, + 'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + 'custom_attributes' => array( + 'pattern' => '.+', + 'autocomplete' => 'off', + 'required' => '', + ), ), 'customer_service_instructions' => array( - 'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ), - 'type' => 'text', - 'default' => '', - 'desc_tip' => true, - 'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + 'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => '', + 'desc_tip' => true, + 'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + 'custom_attributes' => array( + 'pattern' => '.+', + 'autocomplete' => 'off', + 'required' => '', + ), ), ); } diff --git a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php index e84a80b92..34bc13060 100644 --- a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php @@ -10,6 +10,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Processor; use Exception; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use WC_Order; @@ -91,6 +93,20 @@ class AuthorizedPaymentsProcessor { */ private $subscription_helper; + /** + * The amount factory. + * + * @var AmountFactory + */ + private $amount_factory; + + /** + * The reauthorization failure reason. + * + * @var string + */ + private $reauthorization_failure_reason = ''; + /** * AuthorizedPaymentsProcessor constructor. * @@ -100,6 +116,7 @@ class AuthorizedPaymentsProcessor { * @param AuthorizeOrderActionNotice $notice The notice. * @param ContainerInterface $config The settings. * @param SubscriptionHelper $subscription_helper The subscription helper. + * @param AmountFactory $amount_factory The amount factory. */ public function __construct( OrderEndpoint $order_endpoint, @@ -107,7 +124,8 @@ class AuthorizedPaymentsProcessor { LoggerInterface $logger, AuthorizeOrderActionNotice $notice, ContainerInterface $config, - SubscriptionHelper $subscription_helper + SubscriptionHelper $subscription_helper, + AmountFactory $amount_factory ) { $this->order_endpoint = $order_endpoint; @@ -116,6 +134,7 @@ class AuthorizedPaymentsProcessor { $this->notice = $notice; $this->config = $config; $this->subscription_helper = $subscription_helper; + $this->amount_factory = $amount_factory; } /** @@ -249,6 +268,67 @@ class AuthorizedPaymentsProcessor { } } + /** + * Reauthorizes an authorized payment for an WooCommerce order. + * + * @param WC_Order $wc_order The WooCommerce order. + * + * @return string The status or reauthorization id. + */ + public function reauthorize_payment( WC_Order $wc_order ): string { + $this->reauthorization_failure_reason = ''; + + try { + $order = $this->paypal_order_from_wc_order( $wc_order ); + } catch ( Exception $exception ) { + $this->logger->error( 'Could not get PayPal order from WC order: ' . $exception->getMessage() ); + if ( $exception->getCode() === 404 ) { + return self::NOT_FOUND; + } + return self::INACCESSIBLE; + } + + $amount = $this->amount_factory->from_wc_order( $wc_order ); + + $authorizations = $this->all_authorizations( $order ); + $uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations ); + + if ( ! $uncaptured_authorizations ) { + if ( $this->captured_authorizations( ...$authorizations ) ) { + $this->logger->info( 'Authorizations already captured.' ); + return self::ALREADY_CAPTURED; + } + + $this->logger->info( 'Bad authorization.' ); + return self::BAD_AUTHORIZATION; + } + + $authorization = end( $uncaptured_authorizations ); + + try { + $this->payments_endpoint->reauthorize( $authorization->id(), new Money( $amount->value(), $amount->currency_code() ) ); + } catch ( PayPalApiException $exception ) { + $this->reauthorization_failure_reason = $exception->details()[0]->description ?? null; + $this->logger->error( 'Reauthorization failed: ' . $exception->name() . ' | ' . $this->reauthorization_failure_reason ); + return self::FAILED; + + } catch ( Exception $exception ) { + $this->logger->error( 'Failed to capture authorization: ' . $exception->getMessage() ); + return self::FAILED; + } + + return self::SUCCESSFUL; + } + + /** + * The reason for a failed reauthorization. + * + * @return string + */ + public function reauthorization_failure_reason(): string { + return $this->reauthorization_failure_reason; + } + /** * Voids authorizations for the given PayPal order. * @@ -392,4 +472,5 @@ class AuthorizedPaymentsProcessor { } ); } + } diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php index 32569c60b..51a3a741c 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 {
  • %3$s
  • %4$s
  • +
  • %5$s
  • '; $avs_response_order_note_result = sprintf( $avs_response_order_note_result_format, @@ -119,7 +121,9 @@ trait CreditCardOrderInfoHandlingTrait { /* translators: %s is fraud AVS postal match */ sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) ), /* translators: %s is card brand */ - sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) ) + sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) ), + /* translators: %s card last digits */ + sprintf( __( 'Card Last Digits: %s', 'woocommerce-paypal-payments' ), esc_html( $card_last_digits ) ) ); $avs_response_order_note = sprintf( $avs_response_order_note_format, @@ -136,7 +140,13 @@ trait CreditCardOrderInfoHandlingTrait { ); $wc_order->add_order_note( $cvv_response_order_note ); - $meta_details = array_merge( $fraud_responses, array( 'card_brand' => $card_brand ) ); + $meta_details = array_merge( + $fraud_responses, + array( + 'card_brand' => $card_brand, + 'card_last_digits' => $card_last_digits, + ) + ); $wc_order->update_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY, $meta_details ); $wc_order->save_meta_data(); diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php index b18468d7b..62a80456a 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php @@ -45,6 +45,18 @@ trait OrderMetaTrait { $wc_order->update_meta_data( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY, $payment_source ); } + $payer = $order->payer(); + if ( + $payer + && $payment_source + && in_array( $payment_source, 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(); do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 59bb440f1..25a367693 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -65,7 +65,7 @@ return function ( ContainerInterface $container, array $fields ): array { American Express Discover iDEAL - Sofort + BLIK
    Apple Pay diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 18875be0f..8e765a8e9 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway; use Psr\Log\LoggerInterface; use Throwable; +use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; @@ -447,6 +448,53 @@ class WCGatewayModule implements ModuleInterface { delete_transient( 'ppcp_reference_transaction_enabled' ); } ); + + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + add_filter( + 'woocommerce_admin_billing_fields', + function ( $fields ) { + global $theorder; + + if ( ! apply_filters( 'woocommerce_paypal_payments_order_details_show_paypal_email', true ) ) { + return $fields; + } + + if ( ! is_array( $fields ) ) { + return $fields; + } + + if ( ! $theorder instanceof WC_Order ) { + return $fields; + } + + $email = $theorder->get_meta( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY ) ?: ''; + + if ( ! $email ) { + return $fields; + } + + // Is payment source is paypal exclude all non paypal funding sources. + $payment_source = $theorder->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) ?: ''; + $is_paypal_funding_source = ( strpos( $theorder->get_payment_method_title(), '(via PayPal)' ) === false ); + + if ( $payment_source === 'paypal' && ! $is_paypal_funding_source ) { + return $fields; + } + + $fields['paypal_email'] = array( + 'label' => __( 'PayPal email address', 'woocommerce-paypal-payments' ), + 'value' => $email, + 'wrapper_class' => 'form-field-wide', + 'custom_attributes' => array( 'disabled' => 'disabled' ), + ); + + return $fields; + } + ); } /** @@ -614,13 +662,18 @@ class WCGatewayModule implements ModuleInterface { return $order_actions; } - $render = $container->get( 'wcgateway.admin.render-authorize-action' ); + $render_reauthorize = $container->get( 'wcgateway.admin.render-reauthorize-action' ); + $render_authorize = $container->get( 'wcgateway.admin.render-authorize-action' ); + /** * Renders the authorize action in the select field. * * @var RenderAuthorizeAction $render */ - return $render->render( $order_actions, $theorder ); + return $render_reauthorize->render( + $render_authorize->render( $order_actions, $theorder ), + $theorder + ); } ); @@ -637,6 +690,36 @@ class WCGatewayModule implements ModuleInterface { $authorized_payments_processor->capture_authorized_payment( $wc_order ); } ); + + add_action( + 'woocommerce_order_action_ppcp_reauthorize_order', + static function ( WC_Order $wc_order ) use ( $container ) { + $admin_notices = $container->get( 'admin-notices.repository' ); + assert( $admin_notices instanceof Repository ); + + /** + * The authorized payments processor. + * + * @var AuthorizedPaymentsProcessor $authorized_payments_processor + */ + $authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' ); + + if ( $authorized_payments_processor->reauthorize_payment( $wc_order ) !== AuthorizedPaymentsProcessor::SUCCESSFUL ) { + $message = sprintf( + '%1$s %2$s', + esc_html__( 'Reauthorization with PayPal failed: ', 'woocommerce-paypal-payments' ), + $authorized_payments_processor->reauthorization_failure_reason() ?: '' + ); + $admin_notices->persist( new Message( $message, 'error' ) ); + } else { + $admin_notices->persist( new Message( 'Payment reauthorized.', 'info' ) ); + + $wc_order->add_order_note( + __( 'Payment reauthorized.', 'woocommerce-paypal-payments' ) + ); + } + } + ); } /** diff --git a/modules/ppcp-wc-subscriptions/services.php b/modules/ppcp-wc-subscriptions/services.php index bab617b04..dc23fca9f 100644 --- a/modules/ppcp-wc-subscriptions/services.php +++ b/modules/ppcp-wc-subscriptions/services.php @@ -44,7 +44,9 @@ return array( $authorized_payments_processor, $funding_source_renderer, $container->get( 'wc-subscriptions.helpers.real-time-account-updater' ), - $container->get( 'wc-subscriptions.helper' ) + $container->get( 'wc-subscriptions.helper' ), + $container->get( 'api.endpoint.payment-tokens' ), + $container->get( 'vaulting.wc-payment-tokens' ) ); }, 'wc-subscriptions.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository { diff --git a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php index 7e83218e9..695bce551 100644 --- a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php @@ -9,10 +9,12 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcSubscriptions; +use Psr\Log\LoggerInterface; use WC_Order; -use WC_Subscription; use WC_Payment_Tokens; +use WC_Subscription; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; @@ -25,8 +27,8 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenApplePay; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenPayPal; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; -use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenVenmo; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; @@ -132,6 +134,20 @@ class RenewalHandler { */ private $subscription_helper; + /** + * Payment tokens endpoint + * + * @var PaymentTokensEndpoint + */ + private $payment_tokens_endpoint; + + /** + * WooCommerce payments tokens factory. + * + * @var WooCommercePaymentTokens + */ + private $wc_payment_tokens; + /** * RenewalHandler constructor. * @@ -147,6 +163,8 @@ class RenewalHandler { * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. * @param RealTimeAccountUpdaterHelper $real_time_account_updater_helper Real Time Account Updater helper. * @param SubscriptionHelper $subscription_helper Subscription helper. + * @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint. + * @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payments tokens factory. */ public function __construct( LoggerInterface $logger, @@ -160,7 +178,9 @@ class RenewalHandler { AuthorizedPaymentsProcessor $authorized_payments_processor, FundingSourceRenderer $funding_source_renderer, RealTimeAccountUpdaterHelper $real_time_account_updater_helper, - SubscriptionHelper $subscription_helper + SubscriptionHelper $subscription_helper, + PaymentTokensEndpoint $payment_tokens_endpoint, + WooCommercePaymentTokens $wc_payment_tokens ) { $this->logger = $logger; @@ -175,6 +195,8 @@ class RenewalHandler { $this->funding_source_renderer = $funding_source_renderer; $this->real_time_account_updater_helper = $real_time_account_updater_helper; $this->subscription_helper = $subscription_helper; + $this->payment_tokens_endpoint = $payment_tokens_endpoint; + $this->wc_payment_tokens = $wc_payment_tokens; } /** @@ -236,8 +258,26 @@ class RenewalHandler { // Vault v3. $payment_source = null; if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), PayPalGateway::ID ); + $customer_tokens = $this->wc_payment_tokens->customer_tokens( $user_id ); + + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, PayPalGateway::ID ); + + if ( $customer_tokens && empty( $wc_tokens ) ) { + $this->wc_payment_tokens->create_wc_tokens( $customer_tokens, $user_id ); + } + + $customer_token_ids = array(); + foreach ( $customer_tokens as $customer_token ) { + $customer_token_ids[] = $customer_token['id']; + } + + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, PayPalGateway::ID ); foreach ( $wc_tokens as $token ) { + if ( ! in_array( $token->get_token(), $customer_token_ids, true ) ) { + $token->delete(); + continue; + } + $name = 'paypal'; $properties = array( 'vault_id' => $token->get_token(), @@ -270,7 +310,27 @@ class RenewalHandler { } if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), CreditCardGateway::ID ); + $customer_tokens = $this->wc_payment_tokens->customer_tokens( $user_id ); + + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); + + if ( $customer_tokens && empty( $wc_tokens ) ) { + $this->wc_payment_tokens->create_wc_tokens( $customer_tokens, $user_id ); + } + + $customer_token_ids = array(); + foreach ( $customer_tokens as $customer_token ) { + $customer_token_ids[] = $customer_token['id']; + } + + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); + foreach ( $wc_tokens as $token ) { + if ( ! in_array( $token->get_token(), $customer_token_ids, true ) ) { + $token->delete(); + } + } + + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); $last_token = end( $wc_tokens ); if ( $last_token ) { $payment_source = $this->card_payment_source( $last_token->get_token(), $wc_order ); @@ -295,7 +355,7 @@ class RenewalHandler { if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { $card_payment_source = $order->payment_source(); if ( $card_payment_source ) { - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), CreditCardGateway::ID ); + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); $last_token = end( $wc_tokens ); $expiry = $card_payment_source->properties()->expiry ?? ''; $last_digits = $card_payment_source->properties()->last_digits ?? ''; diff --git a/package.json b/package.json index 8df693e72..a0e2d325a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.5.4", + "version": "2.6.1", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index d71087301..bbf1fecfc 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: woocommerce, automattic, inpsyde Tags: woocommerce, paypal, payments, ecommerce, checkout, cart, pay later, apple pay, subscriptions, debit card, credit card, google pay Requires at least: 5.3 -Tested up to: 6.4 +Tested up to: 6.5 Requires PHP: 7.2 -Stable tag: 2.5.4 +Stable tag: 2.6.1 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -179,6 +179,41 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == += 2.6.1 - 2024-04-09 = +* 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 +* Fix - Problem changing the shipping option in block pages #2142 +* Fix - Saved payment token deleted after payment with another saved payment token #2146 +* 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 +* Enhancement - Add filter for disabling permit_multiple_payment_tokens vault attribute #2136 +* Enhancement - Filter to hide PayPal email address not working on order detail #2137 + += 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 +* Fix - Payments with saved card tokens use Capture intent when Authorize is configured #2069 +* Fix - WooPayments multi-currency causing currency mismatch error on Block Cart & Checkout pages #2054 +* Fix - "Must pass createSubscription with intent=subscription" error with PayPal Subscriptions mode #2058 +* Fix - "Proceed to PayPal" button displayed for Free trial PayPal Subscription products when payment token is saved #2041 +* Fix - ACDC payments with new credit card may fail when debugging is enabled (JSON malformed by warning) #2051 +* Enhancement - Add Pay Later Messaging block #1897 +* Enhancement - Submit the form instead of refreshing the page to show the save notice #2081 +* Enhancement - Integrate pay later messaging block with the messaging configurator #2080 +* Enhancement - Reauthorize authorized payments #2062 +* Enhancement - Do not handle VAULT.PAYMENT-TOKEN.CREATED webhook for Vault v3 #2079 +* Enhancement - Improve the messaging configurator styles #2053 +* Enhancement - Ensure PayPal Vaulting is not selected as Subscriptions Mode when Reference Transactions are disabled #2057 +* Enhancement - Pay later messaging configurator & messaging block adjustments #2096 + = 2.5.4 - 2024-02-27 = * Fix - Cannot enable Apple Pay when API credentials were manually created #2015 * Fix - Cart simulation type error #1943 diff --git a/src/FilePathPluginFactory.php b/src/FilePathPluginFactory.php index 69d118fbb..15f4f761d 100644 --- a/src/FilePathPluginFactory.php +++ b/src/FilePathPluginFactory.php @@ -65,7 +65,7 @@ class FilePathPluginFactory implements FilePathPluginFactoryInterface { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - $plugin_data = get_plugin_data( $filePath ); + $plugin_data = get_plugin_data( $filePath, false ); if ( empty( $plugin_data ) ) { throw new UnexpectedValueException( sprintf( diff --git a/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php index f8eedb3bb..65d41e881 100644 --- a/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php @@ -312,11 +312,12 @@ class ItemFactoryTest extends TestCase $result = $testee->from_wc_order($order); $item = current($result); + /** * @var Item $item */ - $this->assertEquals(mb_substr($name, 0, 127), $item->name()); - $this->assertEquals(substr($description, 0, 127), $item->description()); + $this->assertEquals(substr( strip_shortcodes( wp_strip_all_tags( $name ) ), 0, 127 ), $item->name()); + $this->assertEquals(substr( strip_shortcodes( wp_strip_all_tags( $description ) ), 0, 127 ), $item->description()); } public function testFromPayPalResponse() diff --git a/tests/PHPUnit/Vaulting/VaultedCreditCardHandlerTest.php b/tests/PHPUnit/Vaulting/VaultedCreditCardHandlerTest.php index fb728709a..c7d176c8e 100644 --- a/tests/PHPUnit/Vaulting/VaultedCreditCardHandlerTest.php +++ b/tests/PHPUnit/Vaulting/VaultedCreditCardHandlerTest.php @@ -92,6 +92,8 @@ class VaultedCreditCardHandlerTest extends TestCase $customer = Mockery::mock(WC_Customer::class); $payer = Mockery::mock(Payer::class); + $payer->shouldReceive('email_address'); + $this->payerFactory->shouldReceive('from_wc_order') ->andReturn($payer); $this->shippingPreferenceFactory->shouldReceive('from_state') @@ -100,6 +102,7 @@ class VaultedCreditCardHandlerTest extends TestCase $order = Mockery::mock(Order::class); $order->shouldReceive('id')->andReturn('1'); $order->shouldReceive('intent')->andReturn('CAPTURE'); + $order->shouldReceive('payer')->andReturn($payer); $paymentSource = Mockery::mock(PaymentSource::class); $paymentSource->shouldReceive('name')->andReturn('card'); diff --git a/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php index 745d982f7..60b86a643 100644 --- a/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php @@ -4,21 +4,23 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Mockery; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; -use WooCommerce\PayPalCommerce\Onboarding\Environment; -use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; +use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; +use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use function Brain\Monkey\Functions\when; class CreditCardGatewayTest extends TestCase @@ -34,6 +36,8 @@ class CreditCardGatewayTest extends TestCase private $subscriptionHelper; private $captureCardPayment; private $prefix; + private $paymentTokensEndpoint; + private $wcPaymentTokens; private $logger; private $paymentsEndpoint; private $vaultedCreditCardHandler; @@ -56,6 +60,8 @@ class CreditCardGatewayTest extends TestCase $this->subscriptionHelper = Mockery::mock(SubscriptionHelper::class); $this->captureCardPayment = Mockery::mock(CaptureCardPayment::class); $this->prefix = 'some-prefix'; + $this->paymentTokensEndpoint = Mockery::mock(PaymentTokensEndpoint::class); + $this->wcPaymentTokens = Mockery::mock(WooCommercePaymentTokens::class); $this->logger = Mockery::mock(LoggerInterface::class); $this->paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); $this->vaultedCreditCardHandler = Mockery::mock(VaultedCreditCardHandler::class); @@ -84,6 +90,8 @@ class CreditCardGatewayTest extends TestCase $this->orderEndpoint, $this->captureCardPayment, $this->prefix, + $this->paymentTokensEndpoint, + $this->wcPaymentTokens, $this->logger ); } diff --git a/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php index abff51a10..0f70f801c 100644 --- a/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php @@ -89,6 +89,7 @@ private $testee; $order->shouldReceive('id')->andReturn('1'); $order->shouldReceive('intent'); $order->shouldReceive('payment_source'); + $order->shouldReceive('payer'); $this->orderEndpoint ->shouldReceive('create') diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 5239af7dc..041141492 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -6,11 +6,13 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; @@ -44,6 +46,9 @@ class WcGatewayTest extends TestCase private $logger; private $apiShopCountry; private $orderEndpoint; + private $paymentTokensEndpoint; + private $vaultV3Enabled; + private $wcPaymentTokens; public function setUp(): void { parent::setUp(); @@ -88,6 +93,10 @@ class WcGatewayTest extends TestCase $this->logger->shouldReceive('info'); $this->logger->shouldReceive('error'); + + $this->paymentTokensEndpoint = Mockery::mock(PaymentTokensEndpoint::class); + $this->vaultV3Enabled = true; + $this->wcPaymentTokens = Mockery::mock(WooCommercePaymentTokens::class); } private function createGateway() @@ -111,7 +120,10 @@ class WcGatewayTest extends TestCase function ($id) { return 'checkoutnow=' . $id; }, - 'Pay via PayPal' + 'Pay via PayPal', + $this->paymentTokensEndpoint, + $this->vaultV3Enabled, + $this->wcPaymentTokens ); } diff --git a/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php index 5dd7c588f..9f9aa8c3c 100644 --- a/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php @@ -5,6 +5,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor; use Mockery\MockInterface; +use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use Psr\Log\NullLogger; use WC_Order; @@ -69,6 +70,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase $this->config = Mockery::mock(ContainerInterface::class); $this->subscription_helper = Mockery::mock(SubscriptionHelper::class); + $this->amount_factory = Mockery::mock(AmountFactory::class); $this->testee = new AuthorizedPaymentsProcessor( $this->orderEndpoint, @@ -76,7 +78,8 @@ class AuthorizedPaymentsProcessorTest extends TestCase new NullLogger(), $this->notice, $this->config, - $this->subscription_helper + $this->subscription_helper, + $this->amount_factory ); } diff --git a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php index d27ec6ff7..95f1b7b48 100644 --- a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php @@ -93,6 +93,7 @@ class OrderProcessorTest extends TestCase $currentOrder ->shouldReceive('payment_source') ->andReturn(null); + $currentOrder->shouldReceive('payer'); $wcOrder ->shouldReceive('get_meta') @@ -230,6 +231,7 @@ class OrderProcessorTest extends TestCase $currentOrder ->shouldReceive('payment_source') ->andReturn(null); + $currentOrder->shouldReceive('payer'); $wcOrder ->shouldReceive('get_meta') @@ -357,6 +359,7 @@ class OrderProcessorTest extends TestCase $currentOrder ->shouldReceive('purchase_units') ->andReturn([$purchaseUnit]); + $currentOrder->shouldReceive('payer'); $wcOrder ->shouldReceive('get_meta') diff --git a/tests/Playwright/.env.example b/tests/Playwright/.env.example index 3de8add8c..e23512085 100644 --- a/tests/Playwright/.env.example +++ b/tests/Playwright/.env.example @@ -13,7 +13,7 @@ PRODUCT_ID=123 SUBSCRIPTION_URL="/product/sub" -APM_ID="sofort" +APM_ID="paypal" WP_MERCHANT_USER="admin" WP_MERCHANT_PASSWORD="admin" diff --git a/tests/Playwright/tests/save-payment-methods.spec.js b/tests/Playwright/tests/save-payment-methods.spec.js index 4648eebca..f6fb46315 100644 --- a/tests/Playwright/tests/save-payment-methods.spec.js +++ b/tests/Playwright/tests/save-payment-methods.spec.js @@ -37,6 +37,53 @@ test('Save during purchase', async ({page}) => { await expectOrderReceivedPage(page); }); +test('PayPal add payment method', async ({page}) => { + await loginAsCustomer(page); + await page.goto('/my-account/add-payment-method'); + + const popup = await openPaypalPopup(page); + await loginIntoPaypal(popup); + popup.locator('#consentButton').click(); + + await page.waitForURL('/my-account/payment-methods'); +}); + +test('ACDC add payment method', async ({page}) => { + await loginAsCustomer(page); + await page.goto('/my-account/add-payment-method'); + + await page.click("text=Debit & Credit Cards"); + + const creditCardNumber = await page.frameLocator('[title="paypal_card_number_field"]').locator('.card-field-number'); + await creditCardNumber.fill('4005519200000004'); + + const expirationDate = await page.frameLocator('[title="paypal_card_expiry_field"]').locator('.card-field-expiry'); + await expirationDate.fill('01/25'); + + const cvv = await page.frameLocator('[title="paypal_card_cvv_field"]').locator('.card-field-cvv'); + await cvv.fill('123'); + + await page.waitForURL('/my-account/payment-methods'); +}); + +test('PayPal logged-in user free trial subscription without payment token', async ({page}) => { + await loginAsCustomer(page); + + await page.goto('/shop'); + await page.click("text=Sign up now"); + await page.goto('/classic-checkout'); + + const popup = await openPaypalPopup(page); + await loginIntoPaypal(popup); + popup.locator('#consentButton').click(); + + await page.click("text=Proceed to PayPal"); + + const title = await page.locator('.entry-title'); + await expect(title).toHaveText('Order received'); +}) + + diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index e764c3271..b55b1f00d 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,13 +3,14 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.5.4 + * Version: 2.6.1 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.2 + * Requires Plugins: woocommerce * WC requires at least: 3.9 - * WC tested up to: 8.6 + * WC tested up to: 8.7 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -25,14 +26,14 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); define( 'PAYPAL_URL', 'https://www.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2024-02-16' ); +define( 'PAYPAL_INTEGRATION_DATE', '2024-04-03' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); ! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) && define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' ); ! defined( 'CONNECT_WOO_MERCHANT_ID' ) && define( 'CONNECT_WOO_MERCHANT_ID', 'K8SKZ36LQBWXJ' ); ! defined( 'CONNECT_WOO_SANDBOX_MERCHANT_ID' ) && define( 'CONNECT_WOO_SANDBOX_MERCHANT_ID', 'MPMFHQTVMBZ6G' ); -! defined( 'CONNECT_WOO_URL' ) && define( 'CONNECT_WOO_URL', 'https://connect.woocommerce.com/ppc' ); -! defined( 'CONNECT_WOO_SANDBOX_URL' ) && define( 'CONNECT_WOO_SANDBOX_URL', 'https://connect.woocommerce.com/ppcsandbox' ); +! defined( 'CONNECT_WOO_URL' ) && define( 'CONNECT_WOO_URL', 'https://api.woocommerce.com/integrations/ppc' ); +! defined( 'CONNECT_WOO_SANDBOX_URL' ) && define( 'CONNECT_WOO_SANDBOX_URL', 'https://api.woocommerce.com/integrations/ppcsandbox' ); ( function () { $autoload_filepath = __DIR__ . '/vendor/autoload.php'; @@ -97,7 +98,7 @@ define( 'PAYPAL_INTEGRATION_DATE', '2024-02-16' ); */ require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - $plugin_data = get_plugin_data( __DIR__ . '/woocommerce-paypal-payments.php' ); + $plugin_data = get_plugin_data( __DIR__ . '/woocommerce-paypal-payments.php', false ); $plugin_version = $plugin_data['Version'] ?? null; $installed_plugin_version = get_option( 'woocommerce-ppcp-version' ); if ( $installed_plugin_version !== $plugin_version ) {