Merge branch 'trunk' into modularity-full-migration

# Conflicts:
#	modules/ppcp-blocks/src/BlocksModule.php
This commit is contained in:
Pedro Silva 2023-12-19 10:28:47 +00:00
commit 5837d65b18
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
150 changed files with 8479 additions and 3005 deletions

View file

@ -1,3 +1,5 @@
@use "mixins/apm-button" as apm-button;
#place_order.ppcp-hidden {
display: none !important;
}
@ -15,3 +17,24 @@
.ppc-button-wrapper #ppcp-messages:first-child {
padding-top: 10px;
}
// Prevents spacing after button group.
#ppc-button-ppcp-gateway {
line-height: 0;
div[class^="item-"] {
margin-top: 14px;
&:first-child {
margin-top: 0;
}
}
}
#ppc-button-minicart {
line-height: 0;
display: block;
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -0,0 +1,42 @@
@mixin button {
overflow: hidden;
min-width: 0;
max-width: 750px;
line-height: 0;
border-radius: 4px;
// Defaults
height: 45px;
margin-top: 14px;
&.ppcp-button-pill {
border-radius: 50px;
}
&.ppcp-button-minicart {
display: block;
}
.ppcp-width-min & {
height: 35px;
}
.ppcp-width-300 & {
height: 45px;
}
.ppcp-width-500 & {
height: 55px;
}
// No margin on block layout.
.wp-block-woocommerce-checkout &, .wp-block-woocommerce-cart & {
margin: 0;
min-width: 0;
}
.wp-admin & {
pointer-events: none;
}
}

View file

@ -22,6 +22,7 @@ import FormValidator from "./modules/Helper/FormValidator";
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
import buttonModuleWatcher from "./modules/ButtonModuleWatcher";
import MessagesBootstrap from "./modules/ContextBootstrap/MessagesBootstap";
import {apmButtonsInit} from "./modules/Helper/ApmButtons";
// TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough.
@ -145,6 +146,7 @@ const bootstrap = () => {
};
const onSmartButtonsInit = () => {
jQuery(document).trigger('ppcp-smart-buttons-init', this);
buttonsSpinner.unblock();
};
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit);
@ -217,6 +219,8 @@ const bootstrap = () => {
messageRenderer,
);
messagesBootstrap.init();
apmButtonsInit(PayPalCommerceGateway);
};
document.addEventListener(
@ -279,11 +283,12 @@ document.addEventListener(
});
let bootstrapped = false;
let failed = false;
hideOrderButtonIfPpcpGateway();
jQuery(document.body).on('updated_checkout payment_method_selected', () => {
if (bootstrapped) {
if (bootstrapped || failed) {
return;
}
@ -294,6 +299,12 @@ document.addEventListener(
bootstrapped = true;
bootstrap();
}, () => {
failed = true;
setVisibleByClass(ORDER_BUTTON_SELECTOR, true, 'ppcp-hidden');
buttonsSpinner.unblock();
cardsSpinner.unblock();
});
},
);

View file

@ -2,6 +2,7 @@ import 'formdata-polyfill';
import onApprove from '../OnApproveHandler/onApproveForPayNow.js';
import {payerData} from "../Helper/PayerData";
import {getCurrentPaymentMethod} from "../Helper/CheckoutMethodState";
import validateCheckoutForm from "../Helper/CheckoutFormValidation";
class CheckoutActionHandler {
@ -13,7 +14,13 @@ class CheckoutActionHandler {
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
createSubscription: async (data, actions) => {
try {
await validateCheckoutForm(this.config);
} catch (error) {
throw {type: 'form-validation-error'};
}
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
@ -56,6 +63,8 @@ class CheckoutActionHandler {
const paymentMethod = getCurrentPaymentMethod();
const fundingSource = window.ppcpFundingSource;
const savePaymentMethod = !!document.getElementById('wc-ppcp-credit-card-gateway-new-payment-method')?.checked;
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
headers: {
@ -72,7 +81,8 @@ class CheckoutActionHandler {
funding_source: fundingSource,
// send as urlencoded string to handle complex fields via PHP functions the same as normal form submit
form_encoded: new URLSearchParams(formData).toString(),
createaccount: createaccount
createaccount: createaccount,
save_payment_method: savePaymentMethod
})
}).then(function (res) {
return res.json();

View file

@ -26,7 +26,7 @@ const storeToken = (token) => {
sessionStorage.setItem(storageKey, JSON.stringify(token));
}
const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
const dataClientIdAttributeHandler = (scriptOptions, config, callback, errorCallback = null) => {
fetch(config.endpoint, {
method: 'POST',
headers: {
@ -51,6 +51,10 @@ const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
if (typeof callback === 'function') {
callback(paypal);
}
}).catch(err => {
if (typeof errorCallback === 'function') {
errorCallback(err);
}
});
});
}

View file

@ -0,0 +1,114 @@
export const apmButtonsInit = (config, selector = '.ppcp-button-apm') => {
let selectorInContainer = selector;
if (window.ppcpApmButtons) {
return;
}
if (config && config.button) {
// If it's separate gateways, modify wrapper to account for the individual buttons as individual APMs.
const wrapper = config.button.wrapper;
const isSeparateGateways = jQuery(wrapper).children('div[class^="item-"]').length > 0;
if (isSeparateGateways) {
selector += `, ${wrapper} div[class^="item-"]`;
selectorInContainer += `, div[class^="item-"]`;
}
}
window.ppcpApmButtons = new ApmButtons(selector, selectorInContainer);
}
export class ApmButtons {
constructor(selector, selectorInContainer) {
this.selector = selector;
this.selectorInContainer = selectorInContainer;
this.containers = [];
// Reloads button containers.
this.reloadContainers();
// Refresh button layout.
jQuery(window).resize(() => {
this.refresh();
}).resize();
jQuery(document).on('ppcp-smart-buttons-init', () => {
this.refresh();
});
// Observes for new buttons.
(new MutationObserver(this.observeElementsCallback.bind(this)))
.observe(document.body, { childList: true, subtree: true });
}
observeElementsCallback(mutationsList, observer) {
const observeSelector = this.selector + ', .widget_shopping_cart, .widget_shopping_cart_content';
let shouldReload = false;
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.matches && node.matches(observeSelector)) {
shouldReload = true;
}
});
}
}
if (shouldReload) {
this.reloadContainers();
this.refresh();
}
};
reloadContainers() {
jQuery(this.selector).each((index, el) => {
const parent = jQuery(el).parent();
if (!this.containers.some($el => $el.is(parent))) {
this.containers.push(parent);
}
});
console.log('this.containers', this.containers);
}
refresh() {
for (const container of this.containers) {
const $container = jQuery(container);
// Check width and add classes
const width = $container.width();
$container.removeClass('ppcp-width-500 ppcp-width-300 ppcp-width-min');
if (width >= 500) {
$container.addClass('ppcp-width-500');
} else if (width >= 300) {
$container.addClass('ppcp-width-300');
} else {
$container.addClass('ppcp-width-min');
}
// Check first apm button
const $firstElement = $container.children(':visible').first();
// Assign margins to buttons
$container.find(this.selectorInContainer).each((index, el) => {
const $el = jQuery(el);
if ($el.is($firstElement)) {
$el.css('margin-top', `0px`);
return true;
}
const height = $el.height();
$el.css('margin-top', `${Math.round(height * 0.3)}px`);
});
}
}
}

View file

@ -0,0 +1,50 @@
export const cardFieldStyles = (field) => {
const allowedProperties = [
'appearance',
'color',
'direction',
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-alternates',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-weight',
'letter-spacing',
'line-height',
'opacity',
'outline',
'padding',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'text-shadow',
'transition',
'-moz-appearance',
'-moz-osx-font-smoothing',
'-moz-tap-highlight-color',
'-moz-transition',
'-webkit-appearance',
'-webkit-osx-font-smoothing',
'-webkit-tap-highlight-color',
'-webkit-transition',
];
const stylesRaw = window.getComputedStyle(field);
const styles = {};
Object.values(stylesRaw).forEach((prop) => {
if (!stylesRaw[prop] || !allowedProperties.includes(prop)) {
return;
}
styles[prop] = '' + stylesRaw[prop];
});
return styles;
}

View file

@ -0,0 +1,48 @@
import Spinner from "./Spinner";
import FormValidator from "./FormValidator";
import ErrorHandler from "../ErrorHandler";
const validateCheckoutForm = function (config) {
return new Promise(async (resolve, reject) => {
try {
const spinner = new Spinner();
const errorHandler = new ErrorHandler(
config.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const formSelector = config.context === 'checkout' ? 'form.checkout' : 'form#order_review';
const formValidator = config.early_checkout_validation_enabled ?
new FormValidator(
config.ajax.validate_checkout.endpoint,
config.ajax.validate_checkout.nonce,
) : null;
if (!formValidator) {
resolve();
return;
}
formValidator.validate(document.querySelector(formSelector)).then((errors) => {
if (errors.length > 0) {
spinner.unblock();
errorHandler.clear();
errorHandler.messages(errors);
// fire WC event for other plugins
jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] );
reject();
} else {
resolve();
}
});
} catch (error) {
console.error(error);
reject();
}
});
}
export default validateCheckoutForm;

View file

@ -7,10 +7,11 @@ import {keysToCamelCase} from "./Utils";
// This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
isLoading: false,
onLoadedCallbacks: []
onLoadedCallbacks: [],
onErrorCallbacks: [],
};
export const loadPaypalScript = (config, onLoaded) => {
export const loadPaypalScript = (config, onLoaded, onError = null) => {
// If PayPal is already loaded call the onLoaded callback and return.
if (typeof paypal !== 'undefined') {
onLoaded();
@ -19,6 +20,9 @@ export const loadPaypalScript = (config, onLoaded) => {
// Add the onLoaded callback to the onLoadedCallbacks stack.
options.onLoadedCallbacks.push(onLoaded);
if (onError) {
options.onErrorCallbacks.push(onError);
}
// Return if it's still loading.
if (options.isLoading) {
@ -26,6 +30,12 @@ export const loadPaypalScript = (config, onLoaded) => {
}
options.isLoading = true;
const resetState = () => {
options.isLoading = false;
options.onLoadedCallbacks = [];
options.onErrorCallbacks = [];
}
// Callback to be called once the PayPal script is loaded.
const callback = (paypal) => {
widgetBuilder.setPaypal(paypal);
@ -34,8 +44,14 @@ export const loadPaypalScript = (config, onLoaded) => {
onLoadedCallback();
}
options.isLoading = false;
options.onLoadedCallbacks = [];
resetState();
}
const errorCallback = (err) => {
for (const onErrorCallback of options.onErrorCallbacks) {
onErrorCallback(err);
}
resetState();
}
// Build the PayPal script options.
@ -44,12 +60,26 @@ export const loadPaypalScript = (config, onLoaded) => {
// Load PayPal script for special case with data-client-token
if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback);
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);
return;
}
// Adds data-user-id-token to script options.
const userIdToken = config?.save_payment_methods?.id_token;
if(userIdToken) {
scriptOptions['data-user-id-token'] = userIdToken;
}
// Load PayPal script
loadScript(scriptOptions).then(callback);
loadScript(scriptOptions)
.then(callback)
.catch(errorCallback);
}
export const loadPaypalScriptPromise = (config) => {
return new Promise((resolve, reject) => {
loadPaypalScript(config, resolve, reject)
});
}
export const loadPaypalJsScript = (options, buttons, container) => {
@ -57,3 +87,11 @@ export const loadPaypalJsScript = (options, buttons, container) => {
paypal.Buttons(buttons).render(container);
});
}
export const loadPaypalJsScriptPromise = (options) => {
return new Promise((resolve, reject) => {
loadScript(options)
.then(resolve)
.catch(reject);
});
}

View file

@ -0,0 +1,20 @@
export const normalizeStyleForFundingSource = (style, fundingSource) => {
const commonProps = {};
['shape', 'height'].forEach(prop => {
if (style[prop]) {
commonProps[prop] = style[prop];
}
});
switch (fundingSource) {
case 'paypal':
return style;
case 'paylater':
return {
color: style.color,
...commonProps
};
default:
return commonProps;
}
}

View file

@ -1,4 +1,5 @@
import {show} from "../Helper/Hiding";
import {cardFieldStyles} from "../Helper/CardFieldsHelper";
class CardFieldsRenderer {
@ -53,28 +54,28 @@ class CardFieldsRenderer {
if (cardField.isEligible()) {
const nameField = document.getElementById('ppcp-credit-card-gateway-card-name');
if (nameField) {
let styles = this.cardFieldStyles(nameField);
let styles = cardFieldStyles(nameField);
cardField.NameField({style: {'input': styles}}).render(nameField.parentNode);
nameField.remove();
}
const numberField = document.getElementById('ppcp-credit-card-gateway-card-number');
if (numberField) {
let styles = this.cardFieldStyles(numberField);
let styles = cardFieldStyles(numberField);
cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode);
numberField.remove();
}
const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry');
if (expiryField) {
let styles = this.cardFieldStyles(expiryField);
let styles = cardFieldStyles(expiryField);
cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode);
expiryField.remove();
}
const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc');
if (cvvField) {
let styles = this.cardFieldStyles(cvvField);
let styles = cardFieldStyles(cvvField);
cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode);
cvvField.remove();
}
@ -91,65 +92,35 @@ class CardFieldsRenderer {
this.spinner.block();
this.errorHandler.clear();
const paymentToken = document.querySelector('input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked')?.value
if(paymentToken && paymentToken !== 'new') {
fetch(this.defaultConfig.ajax.capture_card_payment.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.defaultConfig.ajax.capture_card_payment.nonce,
payment_token: paymentToken
})
}).then((res) => {
return res.json();
}).then((data) => {
document.querySelector('#place_order').click();
});
return;
}
cardField.submit()
.catch((error) => {
this.spinner.unblock();
console.error(error)
this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid);
})
});
});
}
cardFieldStyles(field) {
const allowedProperties = [
'appearance',
'color',
'direction',
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-alternates',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-weight',
'letter-spacing',
'line-height',
'opacity',
'outline',
'padding',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'text-shadow',
'transition',
'-moz-appearance',
'-moz-osx-font-smoothing',
'-moz-tap-highlight-color',
'-moz-transition',
'-webkit-appearance',
'-webkit-osx-font-smoothing',
'-webkit-tap-highlight-color',
'-webkit-transition',
];
const stylesRaw = window.getComputedStyle(field);
const styles = {};
Object.values(stylesRaw).forEach((prop) => {
if (!stylesRaw[prop] || !allowedProperties.includes(prop)) {
return;
}
styles[prop] = '' + stylesRaw[prop];
});
return styles;
}
disableFields() {}
enableFields() {}
}
export default CardFieldsRenderer;

View file

@ -2,6 +2,7 @@ import merge from "deepmerge";
import {loadScript} from "@paypal/paypal-js";
import {keysToCamelCase} from "../Helper/Utils";
import widgetBuilder from "./WidgetBuilder";
import {normalizeStyleForFundingSource} from "../Helper/Style";
class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
@ -36,16 +37,7 @@ class Renderer {
} else {
// render each button separately
for (const fundingSource of paypal.getFundingSources().filter(s => !(s in enabledSeparateGateways))) {
let style = settings.button.style;
if (fundingSource !== 'paypal') {
style = {
shape: style.shape,
color: style.color,
};
if (fundingSource !== 'paylater') {
delete style.color;
}
}
const style = normalizeStyleForFundingSource(settings.button.style, fundingSource);
this.renderButtons(
settings.button.wrapper,

View file

@ -15,8 +15,10 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -68,8 +70,41 @@ return array(
return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' );
},
// This service may not work correctly when called too early.
'button.context' => static function ( ContainerInterface $container ): string {
$obj = new class() {
use ContextTrait;
/**
* Session handler.
*
* @var SessionHandler
*/
protected $session_handler;
/** Constructor. */
public function __construct() {
// phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic
$this->session_handler = new SessionHandler();
}
/**
* Wrapper for a non-public function.
*/
public function get_context(): string {
// phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic
return $this->context();
}
};
return $obj->get_context();
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' );
if ( $container->get( 'wcgateway.use-place-order-button' )
&& in_array( $container->get( 'button.context' ), array( 'checkout', 'pay-now' ), true )
) {
return new DisabledSmartButton();
}
if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton();
}
@ -84,7 +119,7 @@ return array(
$request_data = $container->get( 'button.request-data' );
$client_id = $container->get( 'button.client_id' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$messages_apply = $container->get( 'button.helper.messages-apply' );
$environment = $container->get( 'onboarding.environment' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
@ -259,8 +294,10 @@ return array(
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
return new ThreeDSecure(
$container->get( 'api.factory.card-authentication-result-factory' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply(

View file

@ -33,8 +33,8 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
@ -272,6 +272,8 @@ class SmartButton implements SmartButtonInterface {
* @return bool
*/
public function render_wrapper(): bool {
$this->init_context();
if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) {
$this->render_button_wrapper_registrar();
$this->render_message_wrapper_registrar();
@ -303,7 +305,13 @@ class SmartButton implements SmartButtonInterface {
add_filter(
'woocommerce_credit_card_form_fields',
function ( array $default_fields, $id ) use ( $subscription_helper ) : array {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) {
if (
is_user_logged_in()
&& $this->settings->has( 'vault_enabled_dcc' )
&& $this->settings->get( 'vault_enabled_dcc' )
&& CreditCardGateway::ID === $id
&& apply_filters( 'woocommerce_paypal_payments_should_render_card_custom_fields', true )
) {
$default_fields['card-vault'] = sprintf(
'<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
@ -638,7 +646,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
&& $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' )
&& $this->dcc_applies->for_country_currency()
&& in_array( $this->context(), array( 'checkout', 'pay-now' ), true );
&& in_array( $this->context(), array( 'checkout', 'pay-now', 'add-payment-method' ), true );
}
/**
@ -1049,30 +1057,39 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'mini_cart_wrapper' => '#ppc-button-minicart',
'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ),
'cancel_wrapper' => '#ppcp-cancel',
'mini_cart_style' => array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
'label' => $this->style_for_context( 'label', 'mini-cart' ),
'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ),
'height' => $this->settings->has( 'button_mini-cart_height' ) && $this->settings->get( 'button_mini-cart_height' ) ? $this->normalize_height( (int) $this->settings->get( 'button_mini-cart_height' ) ) : 35,
'mini_cart_style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
'label' => $this->style_for_context( 'label', 'mini-cart' ),
'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ),
'height' => $this->normalize_height( $this->style_for_context( 'height', 'mini-cart', 35 ), 25, 55 ),
)
),
'style' => array(
'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ),
'label' => $this->style_for_context( 'label', $this->context() ),
'tagline' => $this->style_for_context( 'tagline', $this->context() ),
'style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ),
'label' => $this->style_for_context( 'label', $this->context() ),
'tagline' => $this->style_for_context( 'tagline', $this->context() ),
'height' => in_array( $this->context(), array( 'cart-block', 'checkout-block' ), true )
? $this->normalize_height( $this->style_for_context( 'height', $this->context(), 48 ), 40, 55 )
: null,
)
),
),
'separate_buttons' => array(
'card' => array(
'id' => CardButtonGateway::ID,
'wrapper' => '#ppc-button-' . CardButtonGateway::ID,
'style' => array(
'shape' => $this->style_for_apm( 'shape', 'card' ),
'color' => $this->style_for_apm( 'color', 'card', 'black' ),
'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal',
'style' => $this->normalize_style(
array(
'shape' => $this->style_for_apm( 'shape', 'card' ),
'color' => $this->style_for_apm( 'color', 'card', 'black' ),
'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal',
)
),
),
),
@ -1143,13 +1160,6 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$localize['pay_now'] = $this->pay_now_script_data();
}
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
$localize['button']['mini_cart_style']['tagline'] = false;
}
if ( $this->style_for_context( 'layout', $this->context() ) !== 'horizontal' ) {
$localize['button']['style']['tagline'] = false;
}
if ( $this->is_paypal_continuation() ) {
$order = $this->session_handler->order();
assert( $order !== null );
@ -1160,7 +1170,8 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
}
$this->request_data->dequeue_nonce_fix();
return $localize;
return apply_filters( 'woocommerce_paypal_payments_localized_script_data', $localize );
}
/**
@ -1269,9 +1280,12 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
}
if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) {
$disable_funding = array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' )
$disable_funding = array_merge(
$disable_funding,
array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' )
)
);
}
@ -1410,12 +1424,14 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
*
* @param string $style The name of the style property.
* @param string $context The context.
* @param ?mixed $default The default value.
*
* @return string
* @return string|int
*/
private function style_for_context( string $style, string $context ): string {
// Use the cart/checkout styles for blocks.
$context = str_replace( '-block', '', $context );
private function style_for_context( string $style, string $context, $default = null ) {
if ( $context === 'checkout-block' ) {
$context = 'checkout-block-express';
}
$defaults = array(
'layout' => 'vertical',
@ -1433,6 +1449,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->get_style_value( "button_{$context}_${style}" )
?? $this->get_style_value( "button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->normalize_style_value( $defaults[ $style ] ?? '' );
}
@ -1443,9 +1460,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* @param string $apm The APM name, such as 'card'.
* @param ?mixed $default The default value.
*
* @return string
* @return string|int
*/
private function style_for_apm( string $style, string $apm, $default = null ): string {
private function style_for_apm( string $style, string $apm, $default = null ) {
return $this->get_style_value( "${apm}_button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->style_for_context( $style, 'checkout' );
@ -1455,9 +1472,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Returns the style property value or null.
*
* @param string $key The style property key in the settings.
* @return string|null
* @return string|int|null
*/
private function get_style_value( string $key ): ?string {
private function get_style_value( string $key ) {
if ( ! $this->settings->has( $key ) ) {
return null;
}
@ -1468,27 +1485,49 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Converts the style property value to string.
*
* @param mixed $value The style property value.
* @return string
* @return string|int
*/
private function normalize_style_value( $value ): string {
private function normalize_style_value( $value ) {
if ( is_bool( $value ) ) {
$value = $value ? 'true' : 'false';
}
if ( is_int( $value ) ) {
return $value;
}
return (string) $value;
}
/**
* Returns a value between 25 and 55.
* Fixes the style.
*
* @param int $height The input value.
* @param array $style The style properties.
* @return array
*/
private function normalize_style( array $style ): array {
if ( array_key_exists( 'tagline', $style ) && ( ! array_key_exists( 'layout', $style ) || $style['layout'] !== 'horizontal' ) ) {
$style['tagline'] = false;
}
if ( array_key_exists( 'height', $style ) && ! $style['height'] ) {
unset( $style['height'] );
}
return $style;
}
/**
* Returns a number between min and max.
*
* @param mixed $height The input value.
* @param int $min The minimum value.
* @param int $max The maximum value.
* @return int The normalized output value.
*/
private function normalize_height( int $height ): int {
if ( $height < 25 ) {
return 25;
private function normalize_height( $height, int $min, int $max ): int {
$height = (int) $height;
if ( $height < $min ) {
return $min;
}
if ( $height > 55 ) {
return 55;
if ( $height > $max ) {
return $max;
}
return $height;

View file

@ -14,6 +14,7 @@ use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
@ -144,10 +145,11 @@ class ApproveOrderEndpoint implements EndpointInterface {
$order = $this->api_endpoint->order( $data['order_id'] );
if ( $order->payment_source() && $order->payment_source()->card() ) {
$payment_source = $order->payment_source();
if ( $payment_source && $payment_source->name() === 'card' ) {
if ( $this->settings->has( 'disable_cards' ) ) {
$disabled_cards = (array) $this->settings->get( 'disable_cards' );
$card = strtolower( $order->payment_source()->card()->brand() );
$card = strtolower( $payment_source->properties()->brand ?? '' );
if ( 'master_card' === $card ) {
$card = 'mastercard';
}

View file

@ -25,12 +25,11 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -305,7 +304,7 @@ class CreateOrderEndpoint implements EndpointInterface {
}
try {
$order = $this->create_paypal_order( $wc_order );
$order = $this->create_paypal_order( $wc_order, $payment_method, $data );
} catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
throw $exception;
@ -416,6 +415,8 @@ class CreateOrderEndpoint implements EndpointInterface {
* Creates the order in the PayPal, uses data from WC order if provided.
*
* @param \WC_Order|null $wc_order WC order to get data from.
* @param string $payment_method WC payment method.
* @param array $data Request data.
*
* @return Order Created PayPal order.
*
@ -423,7 +424,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @throws PayPalApiException If create order request fails.
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
private function create_paypal_order( \WC_Order $wc_order = null ): Order {
private function create_paypal_order( \WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order {
assert( $this->purchase_unit instanceof PurchaseUnit );
$funding_source = $this->parsed_request_data['funding_source'] ?? '';
@ -465,7 +466,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$payer,
null,
'',
$action
$action,
$payment_method,
$data
);
} catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal,

View file

@ -12,6 +12,39 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
trait ContextTrait {
/**
* Initializes context preconditions like is_cart() and is_checkout().
*
* @return void
*/
protected function init_context(): void {
if ( ! apply_filters( 'woocommerce_paypal_payments_block_classic_compat', true ) ) {
return;
}
/**
* Activate is_checkout() on woocommerce/classic-shortcode checkout blocks.
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'woocommerce_is_checkout',
function ( $is_checkout ) {
if ( $is_checkout ) {
return $is_checkout;
}
return has_block( 'woocommerce/classic-shortcode {"shortcode":"checkout"}' );
}
);
// Activate is_cart() on woocommerce/classic-shortcode cart blocks.
if ( ! is_cart() && is_callable( 'wc_maybe_define_constant' ) ) {
if ( has_block( 'woocommerce/classic-shortcode' ) && ! has_block( 'woocommerce/classic-shortcode {"shortcode":"checkout"}' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
}
}
/**
* Checks WC is_checkout() + WC checkout ajax requests.
*/
@ -94,6 +127,10 @@ trait ContextTrait {
return 'checkout';
}
if ( $this->is_add_payment_method_page() ) {
return 'add-payment-method';
}
return 'mini-cart';
}
@ -125,6 +162,11 @@ trait ContextTrait {
* @return bool
*/
private function is_paypal_continuation(): bool {
/**
* Property is already defined in trait consumers.
*
* @psalm-suppress UndefinedThisPropertyFetch
*/
$order = $this->session_handler->order();
if ( ! $order ) {
return false;
@ -137,7 +179,7 @@ trait ContextTrait {
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
if ( $source && $source->name() === 'card' ) {
return false; // Ignore for DCC.
}
@ -147,4 +189,22 @@ trait ContextTrait {
return true;
}
/**
* Checks whether current page is Add payment method.
*
* @return bool
*/
private function is_add_payment_method_page(): bool {
/**
* Needed for WordPress `query_vars`.
*
* @psalm-suppress InvalidGlobal
*/
global $wp;
$page_id = wc_get_page_id( 'myaccount' );
return $page_id && is_page( $page_id ) && isset( $wp->query_vars['add-payment-method'] );
}
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult as AuthResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
/**
* Class ThreeDSecure
@ -23,6 +24,13 @@ class ThreeDSecure {
const REJECT = 2;
const RETRY = 3;
/**
* Card authentication result factory.
*
* @var CardAuthenticationResultFactory
*/
private $card_authentication_result_factory;
/**
* The logger.
*
@ -33,10 +41,15 @@ class ThreeDSecure {
/**
* ThreeDSecure constructor.
*
* @param LoggerInterface $logger The logger.
* @param CardAuthenticationResultFactory $card_authentication_result_factory Card authentication result factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
public function __construct(
CardAuthenticationResultFactory $card_authentication_result_factory,
LoggerInterface $logger
) {
$this->logger = $logger;
$this->card_authentication_result_factory = $card_authentication_result_factory;
}
/**
@ -49,29 +62,36 @@ class ThreeDSecure {
* @return int
*/
public function proceed_with_order( Order $order ): int {
if ( ! $order->payment_source() ) {
return self::NO_DECISION;
}
if ( ! $order->payment_source()->card() ) {
return self::NO_DECISION;
}
if ( ! $order->payment_source()->card()->authentication_result() ) {
$payment_source = $order->payment_source();
if ( ! $payment_source ) {
return self::NO_DECISION;
}
$result = $order->payment_source()->card()->authentication_result();
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
return self::PROCCEED;
if ( ! $payment_source->properties()->brand ?? '' ) {
return self::NO_DECISION;
}
if ( ! $payment_source->properties()->authentication_result ?? '' ) {
return self::NO_DECISION;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
return self::RETRY;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
return $this->no_liability_shift( $result );
$authentication_result = $payment_source->properties()->authentication_result ?? null;
if ( $authentication_result ) {
$result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result );
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
return self::PROCCEED;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
return self::RETRY;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
return $this->no_liability_shift( $result );
}
}
return self::NO_DECISION;
}