Merge branch 'trunk' into PCP-2085-next-payment-status-not-updated-when-using-pay-pal-subscriptions

This commit is contained in:
Emili Castells Guasch 2024-04-12 10:05:58 +02:00
commit aeecb72589
68 changed files with 1695 additions and 330 deletions

View file

@ -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.

View file

@ -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

View file

@ -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" )();
}

View file

@ -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;
}
);
}
/**

View file

@ -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,
);
}
}

View file

@ -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;
}
}

View file

@ -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(),

View file

@ -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(),

View file

@ -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;
}
}

View file

@ -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.
*

View file

@ -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(),
'',

View file

@ -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 ) ?: '';
}
/**

View file

@ -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;
}

View file

@ -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}
/>
);
}

View file

@ -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 (

View file

@ -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;

View file

@ -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());
}

View file

@ -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' )
);
},

View file

@ -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 '';
}

View file

@ -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 );

View file

@ -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();
/**

View file

@ -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;
}
/**

View file

@ -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,
),
),
);

View file

@ -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,

View file

@ -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 } ) {
</div>
}
if (!PcpPayLaterBlock.placementEnabled) {
return <div {...props}>
<div className={'block-editor-warning__contents'}>
<h3>{__('PayPal Pay Later Messaging', 'woocommerce-paypal-payments')}</h3>
<p className={'block-editor-warning__message'}>{__('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')}</p>
<div className={'class="block-editor-warning__actions"'}>
<span className={'block-editor-warning__action'}>
<a href={PcpPayLaterBlock.payLaterSettingsUrl} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</a>
</span>
<span className={'block-editor-warning__action'}>
<button onClick={() => wp.data.dispatch( 'core/block-editor' ).removeBlock(clientId)} type={'button'} className={'components-button is-secondary'}>
{__('Remove Block', 'woocommerce-paypal-payments')}
</button>
</span>
</div>
</div>
</div>
}
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 && (<SelectControl
label={__('Logo', 'woocommerce-paypal-payments')}
options={[
{ label: __('Primary', 'woocommerce-paypal-payments'), value: 'primary' },
{ label: __('Alternative', 'woocommerce-paypal-payments'), value: 'alternative' },
{ label: __('Full logo', 'woocommerce-paypal-payments'), value: 'primary' },
{ label: __('Monogram', 'woocommerce-paypal-payments'), value: 'alternative' },
{ label: __('Inline', 'woocommerce-paypal-payments'), value: 'inline' },
{ label: __('None', 'woocommerce-paypal-payments'), value: 'none' },
{ label: __('Message only', 'woocommerce-paypal-payments'), value: 'none' },
]}
value={logo}
onChange={(value) => setAttributes({logo: value})}
@ -129,24 +151,31 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
{ !isFlex && (<SelectControl
label={__('Text Color', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Black', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'Black / Blue logo', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White / White logo', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'Monochrome', 'woocommerce-paypal-payments' ), value: 'monochrome' },
{ label: __( 'Grayscale', 'woocommerce-paypal-payments' ), value: 'grayscale' },
{ label: __( 'Black / Gray logo', 'woocommerce-paypal-payments' ), value: 'grayscale' },
]}
value={color}
onChange={(value) => setAttributes({color: value})}
/>)}
{ !isFlex && (<SelectControl
label={__('Text Size', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Small', 'woocommerce-paypal-payments' ), value: '12' },
{ label: __( 'Medium', 'woocommerce-paypal-payments' ), value: '14' },
{ label: __( 'Large', 'woocommerce-paypal-payments' ), value: '16' },
]}
value={size}
onChange={(value) => setAttributes({size: value})}
/>)}
{ isFlex && (<SelectControl
label={__('Color', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Blue', 'woocommerce-paypal-payments' ), value: 'blue' },
{ label: __( 'Black', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'White no border', 'woocommerce-paypal-payments' ), value: 'white-no-border' },
{ label: __( 'Gray', 'woocommerce-paypal-payments' ), value: 'gray' },
{ label: __( 'Monochrome', 'woocommerce-paypal-payments' ), value: 'monochrome' },
{ label: __( 'Grayscale', 'woocommerce-paypal-payments' ), value: 'grayscale' },
{ label: __( 'White (no border)', 'woocommerce-paypal-payments' ), value: 'white-no-border' },
]}
value={flexColor}
onChange={(value) => setAttributes({flexColor: value})}
@ -154,8 +183,6 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
{ isFlex && (<SelectControl
label={__('Ratio', 'woocommerce-paypal-payments')}
options={[
{ label: __( '1x1', 'woocommerce-paypal-payments' ), value: '1x1' },
{ label: __( '1x4', 'woocommerce-paypal-payments' ), value: '1x4' },
{ label: __( '8x1', 'woocommerce-paypal-payments' ), value: '8x1' },
{ label: __( '20x1', 'woocommerce-paypal-payments' ), value: '20x1' },
]}
@ -167,12 +194,11 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
help={ __( 'Used for the analytics dashboard in the merchant account.', 'woocommerce-paypal-payments' ) }
options={ [
{ label: __( 'Detect automatically', 'woocommerce-paypal-payments' ), value: 'auto' },
{ label: __( 'Product Page', 'woocommerce-paypal-payments' ), value: 'product' },
{ label: __( 'Cart', 'woocommerce-paypal-payments' ), value: 'cart' },
{ label: __( 'Payment', 'woocommerce-paypal-payments' ), value: 'payment' },
{ label: __( 'Product', 'woocommerce-paypal-payments' ), value: 'product' },
{ label: __( 'Product list', 'woocommerce-paypal-payments' ), value: 'product-list' },
{ label: __( 'Checkout', 'woocommerce-paypal-payments' ), value: 'checkout' },
{ label: __( 'Home', 'woocommerce-paypal-payments' ), value: 'home' },
{ label: __( 'Category', 'woocommerce-paypal-payments' ), value: 'category' },
{ label: __( 'Shop', 'woocommerce-paypal-payments' ), value: 'shop' },
] }
value={ placement }
onChange={ ( value ) => setAttributes( { placement: value } ) }

View file

@ -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;

View file

@ -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&section=ppcp-gateway' ),
'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ),
'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=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&section=ppcp-gateway&ppcp-tab=ppcp-pay-later' ),
)
);

View file

@ -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{

View file

@ -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,

View file

@ -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 '';
}
}
}

View file

@ -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 ) ) {

View file

@ -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

View file

@ -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();

View file

@ -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')
);

View file

@ -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' )
);
},
);

View file

@ -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();

View file

@ -0,0 +1,90 @@
<?php
/**
* Create payment token for guest user.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class UpdateCustomerId
*/
class CreatePaymentTokenForGuest implements EndpointInterface {
const ENDPOINT = 'ppc-update-customer-id';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The payment method tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* CreatePaymentToken constructor.
*
* @param RequestData $request_data The request data.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint The payment method tokens endpoint.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint
) {
$this->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;
}
}

View file

@ -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 ) {

View file

@ -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' );
},
);

View file

@ -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(),
),
)
);
}
}
}
}

View file

@ -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' )
);
},
);

View file

@ -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
*/

View file

@ -0,0 +1,67 @@
<?php
/**
* Renders the order action "Reauthorize PayPal payment"
*
* @package WooCommerce\PayPalCommerce\WcGateway\Admin
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
/**
* Class RenderReauthorizeAction
*/
class RenderReauthorizeAction {
/**
* The capture info column.
*
* @var OrderTablePaymentStatusColumn
*/
private $column;
/**
* PaymentStatusOrderDetail constructor.
*
* @param OrderTablePaymentStatusColumn $column The capture info column.
*/
public function __construct( OrderTablePaymentStatusColumn $column ) {
$this->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 );
}
}

View file

@ -7,7 +7,7 @@
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;

View file

@ -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 ]
);

View file

@ -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 );

View file

@ -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 );
}

View file

@ -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' => '',
),
),
);
}

View file

@ -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 {
}
);
}
}

View file

@ -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 {
<li>%3$s</li>
</ul>
<li>%4$s</li>
<li>%5$s</li>
</ul>';
$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();

View file

@ -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 );

View file

@ -65,7 +65,7 @@ return function ( ContainerInterface $container, array $fields ): array {
<a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="American Express" src="' . esc_url( $module_url ) . 'assets/images/amex.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="Discover" src="' . esc_url( $module_url ) . 'assets/images/discover.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="iDEAL" src="' . esc_url( $module_url ) . 'assets/images/ideal-dark.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="Sofort" src="' . esc_url( $module_url ) . 'assets/images/sofort.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="BLIK" src="' . esc_url( $module_url ) . 'assets/images/blik.svg"/></a>
</div>
<div class="ppcp-onboarding-header-apm-logos">
<a href="https://woo.com/document/woocommerce-paypal-payments/#apple-pay" target="_blank"><img alt="Apple Pay" src="' . esc_url( $module_url ) . 'assets/images/button-Apple-Pay.png"/></a>

View file

@ -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' )
);
}
}
);
}
/**

View file

@ -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 {

View file

@ -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 ?? '';

View file

@ -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",

View file

@ -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

View file

@ -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(

View file

@ -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()

View file

@ -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');

View file

@ -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
);
}

View file

@ -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')

View file

@ -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
);
}

View file

@ -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
);
}

View file

@ -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')

View file

@ -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"

View file

@ -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');
})

View file

@ -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 ) {