Merge branch 'trunk' into PCP-1486-paylater-block

This commit is contained in:
Alex P 2023-12-20 09:00:57 +02:00
commit c83975f293
No known key found for this signature in database
GPG key ID: 54487A734A204D71
54 changed files with 2315 additions and 1600 deletions

View file

@ -1,5 +1,18 @@
*** Changelog ***
= 2.4.3 - xxxx-xx-xx =
* Fix - PayPal Subscription initiated without a WooCommerce order #1907
* Fix - Block Checkout reloads when submitting order with empty fields #1904
* Fix - "Send checkout billing and shipping data to Apple Pay" displayed when Apple Pay is disabled #1883
* Fix - "Order does not contain intent" error for ACDC renewals when triggering 3D Secure #1888
* Enhancement - Add button to reload feature eligibility status from Connection tab #1902
* Enhancement - Apple Pay validation message improvements #1901
* Enhancement - Improve support for Classic Cart & Classic Checkout blocks #1894
* Enhancement - Ensure uniform button appearance for PayPal, Google Pay, and Apple Pay buttons #1900
* Enhancement - remove string translations for package tracking carriers from repository #1885
* Enhancement - Incorrect margins when PayPal buttons are rendered as separate gateways. #1908
* Feature preview - Save payment methods (Vault v3) integration #1779
= 2.4.2 - 2023-12-04 =
* Fix - Action callback arguments count in ShipStation tracking integration #1841
* Fix - Google Pay scripts loading on unrelated admin pages #1834

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -174,14 +175,15 @@ class OrderEndpoint {
/**
* Creates an order.
*
* @param PurchaseUnit[] $items The purchase unit items for the order.
* @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values.
* @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token.
* @param string $paypal_request_id The PayPal request id.
* @param string $user_action The user action.
* @param string $payment_method WC payment method.
* @param array $request_data Request data.
* @param PurchaseUnit[] $items The purchase unit items for the order.
* @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values.
* @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token.
* @param string $paypal_request_id The PayPal request id.
* @param string $user_action The user action.
* @param string $payment_method WC payment method.
* @param array $request_data Request data.
* @param PaymentSource|null $payment_source The payment source.
*
* @return Order
* @throws RuntimeException If the request fails.
@ -194,7 +196,8 @@ class OrderEndpoint {
string $paypal_request_id = '',
string $user_action = ApplicationContext::USER_ACTION_CONTINUE,
string $payment_method = '',
array $request_data = array()
array $request_data = array(),
PaymentSource $payment_source = null
): Order {
$bearer = $this->bearer->bearer();
$data = array(
@ -221,6 +224,11 @@ class OrderEndpoint {
if ( $payment_token ) {
$data['payment_source']['token'] = $payment_token->to_array();
}
if ( $payment_source ) {
$data['payment_source'] = array(
$payment_source->name() => $payment_source->properties(),
);
}
/**
* The filter can be used to modify the order creation request body data.

View file

@ -26,17 +26,17 @@ class PaymentSource {
/**
* Payment source properties.
*
* @var stdClass
* @var object
*/
private $properties;
/**
* PaymentSource constructor.
*
* @param string $name Payment source name.
* @param stdClass $properties Payment source properties.
* @param string $name Payment source name.
* @param object $properties Payment source properties.
*/
public function __construct( string $name, stdClass $properties ) {
public function __construct( string $name, object $properties ) {
$this->name = $name;
$this->properties = $properties;
}
@ -53,9 +53,9 @@ class PaymentSource {
/**
* Payment source properties.
*
* @return stdClass
* @return object
*/
public function properties(): stdClass {
public function properties(): object {
return $this->properties;
}
}

View file

@ -49,7 +49,9 @@ return array(
// Domain validation.
$domain_validation_text = __( 'Status: Domain validation failed ❌', 'woocommerce-paypal-payments' );
if ( $container->get( 'applepay.is_validated' ) ) {
if ( ! $container->get( 'applepay.has_validated' ) ) {
$domain_validation_text = __( 'The domain has not yet been validated. Use the Apple Pay button to validate the domain ❌', 'woocommerce-paypal-payments' );
} elseif ( $container->get( 'applepay.is_validated' ) ) {
$domain_validation_text = __( 'Status: Domain successfully validated ✔️', 'woocommerce-paypal-payments' );
}
@ -157,6 +159,7 @@ return array(
->action_visible( 'applepay_button_color' )
->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' )
->to_array(),
)
),

View file

@ -1,58 +1,44 @@
#applepay-container, .ppcp-button-applepay {
.ppcp-button-applepay {
// Should replicate apm-button.scss sizes.
--apple-pay-button-height: 45px;
--apple-pay-button-min-height: 40px;
--apple-pay-button-min-height: 35px;
--apple-pay-button-width: 100%;
--apple-pay-button-max-width: 750px;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-overflow: hidden;
margin:7px 0;
.ppcp-width-min & {
--apple-pay-button-height: 35px;
}
.ppcp-width-300 & {
--apple-pay-button-height: 45px;
}
.ppcp-width-500 & {
--apple-pay-button-height: 55px;
}
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
&.ppcp-button-minicart {
--apple-pay-button-display: block;
--apple-pay-button-height: 40px;
}
}
.woocommerce-checkout {
#applepay-container, .ppcp-button-applepay {
margin-top: 0;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-height: 45px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
.ppcp-button-applepay {
--apple-pay-button-margin: 0;
.ppcp-has-applepay-block {
.wp-block-woocommerce-checkout {
#applepay-container, .ppcp-button-applepay {
margin: 0;
--apple-pay-button-margin: 0;
--apple-pay-button-height: 48px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.wp-block-woocommerce-cart {
#applepay-container, .ppcp-button-applepay {
margin: 0;
--apple-pay-button-margin: 0;
--apple-pay-button-height: 48px;
apple-pay-button {
min-width: 0;
width: 100%;
--apple-pay-button-width-default: 100%;
}
}
}
.wp-admin {
.ppcp-button-applepay {
pointer-events: none;
}
&.ppcp-non-ios-device {
.ppcp-button-applepay {
apple-pay-button {

View file

@ -5,10 +5,13 @@ import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/Butto
import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator";
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import {apmButtonsInit} from "../../../ppcp-button/resources/js/modules/Helper/ApmButtons";
class ApplepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) {
apmButtonsInit(ppcpConfig);
this.isInitialized = false;
this.context = context;
@ -60,7 +63,7 @@ class ApplepayButton {
this.initEventHandlers();
this.isInitialized = true;
this.applePayConfig = config;
const isEligible = this.applePayConfig.isEligible;
const isEligible = (this.applePayConfig.isEligible && window.ApplePaySession) || this.buttonConfig.is_admin;
if (isEligible) {
this.fetchTransactionInfo().then(() => {
@ -84,6 +87,10 @@ class ApplepayButton {
});
}
});
} else {
jQuery('#' + this.buttonConfig.button.wrapper).hide();
jQuery('#' + this.buttonConfig.button.mini_cart_wrapper).hide();
jQuery('#express-payment-method-ppcp-applepay').hide();
}
}
@ -179,13 +186,13 @@ class ApplepayButton {
appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`;
}
jQuery('#' + wrapper).addClass('ppcp-button-' + ppcpStyle.shape);
const $wrapper = jQuery('#' + wrapper);
$wrapper.addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
jQuery('#' + wrapper).css('--apple-pay-button-height', `${ppcpStyle.height}px`)
$wrapper.css('--apple-pay-button-height', `${ppcpStyle.height}px`)
$wrapper.css('height', `${ppcpStyle.height}px`)
}
jQuery(wrapper).append(appleContainer);
}
//------------------------

View file

@ -65,7 +65,7 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig.button.wrapper = selector.replace('#', '');
applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-applepay"></div>`;
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-applepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);

View file

@ -23,12 +23,6 @@ const ApplePayComponent = () => {
const manager = new ApplepayManager(buttonConfig, ppcpConfig);
manager.init();
};
useEffect(() => {
const bodyClass = 'ppcp-has-applepay-block';
if (!document.body.classList.contains(bodyClass)) {
document.body.classList.add(bodyClass);
}
}, []);
useEffect(() => {
// Load ApplePay SDK
@ -50,7 +44,7 @@ const ApplePayComponent = () => {
}, [paypalLoaded, applePayLoaded]);
return (
<div id={buttonConfig.button.wrapper.replace('#', '')}></div>
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-apm ppcp-button-applepay"></div>
);
}

View file

@ -62,6 +62,11 @@ return array(
);
},
'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' );
},
'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false;

View file

@ -968,7 +968,7 @@ class ApplePayButton implements ButtonInterface {
add_action(
$render_placeholder,
function () {
echo '<span id="applepay-container-minicart" class="ppcp-button-applepay ppcp-button-minicart"></span>';
echo '<span id="applepay-container-minicart" class="ppcp-button-apm ppcp-button-applepay ppcp-button-minicart"></span>';
},
21
);
@ -981,7 +981,7 @@ class ApplePayButton implements ButtonInterface {
*/
protected function applepay_button(): void {
?>
<div id="applepay-container">
<div id="applepay-container" class="ppcp-button-apm ppcp-button-applepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div>
<?php

View file

@ -149,6 +149,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
@ -204,6 +205,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
@ -252,6 +254,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => true,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),

View file

@ -1,6 +1,7 @@
<?php
/**
* ApmApplies helper.
* Checks if ApplePay is available for a given country and currency.
*
* @package WooCommerce\PayPalCommerce\ApplePay\Helper
*/
@ -15,7 +16,7 @@ namespace WooCommerce\PayPalCommerce\Applepay\Helper;
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for DCC.
* The matrix which countries and currency combinations can be used for ApplePay.
*
* @var array
*/
@ -38,7 +39,7 @@ class ApmApplies {
/**
* ApmApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for ApplePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/

View file

@ -0,0 +1,15 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
li[id^="express-payment-method-ppcp-"] {
line-height: 0;
// Set min-width to 0 as the buttons need to fit in a tight grid.
.paypal-buttons {
min-width: 0 !important;
}
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -34,6 +34,7 @@ const PayPalComponent = ({
const {responseTypes} = emitResponse;
const [paypalOrder, setPaypalOrder] = useState(null);
const [gotoContinuationOnError, setGotoContinuationOnError] = useState(false);
const [paypalScriptLoaded, setPaypalScriptLoaded] = useState(false);
@ -165,6 +166,7 @@ const PayPalComponent = ({
if (config.finalReviewEnabled) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError(true);
onSubmit();
}
} catch (err) {
@ -183,7 +185,7 @@ const PayPalComponent = ({
if (config.scriptData.continuation) {
return true;
}
if (wp.data.select('wc/store/validation').hasValidationErrors()) {
if (gotoContinuationOnError && wp.data.select('wc/store/validation').hasValidationErrors()) {
location.href = getCheckoutRedirectUrl();
return { type: responseTypes.ERROR };
}
@ -191,7 +193,7 @@ const PayPalComponent = ({
return true;
});
return unsubscribe;
}, [onCheckoutValidation] );
}, [onCheckoutValidation, gotoContinuationOnError] );
const handleClick = (data, actions) => {
if (isEditing) {

View file

@ -88,6 +88,28 @@ class BlocksModule implements ModuleInterface {
$endpoint->handle_request();
}
);
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c ) {
if ( ! has_block( 'woocommerce/checkout' ) && ! has_block( 'woocommerce/cart' ) ) {
return;
}
$module_url = $c->get( 'blocks.url' );
$asset_version = $c->get( 'ppcp.asset-version' );
wp_register_style(
'wc-ppcp-blocks',
untrailingslashit( $module_url ) . '/assets/css/gateway.css',
array(),
$asset_version
);
wp_enqueue_style( 'wc-ppcp-blocks' );
}
);
}
/**

View file

@ -9,7 +9,8 @@ module.exports = {
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'checkout-block': path.resolve('./resources/js/checkout-block.js')
'checkout-block': path.resolve('./resources/js/checkout-block.js'),
"gateway": path.resolve('./resources/css/gateway.scss')
},
output: {
path: path.resolve(__dirname, 'assets/'),

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(

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

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

@ -89,3 +89,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

@ -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,8 +92,8 @@ 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 !== 'new') {
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',
@ -118,57 +119,6 @@ class CardFieldsRenderer {
});
}
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() {}
}

View file

@ -273,6 +273,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();

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

View file

@ -1,45 +1,9 @@
.ppcp-button-googlepay {
margin: 7px 0;
overflow: hidden;
min-height: 40px;
height: 45px;
&.ppcp-button-pill {
border-radius: 50px;
}
&.ppcp-button-minicart {
display: block;
height: 40px;
}
}
.woocommerce-checkout {
.ppcp-button-googlepay {
margin-top: 0;
}
}
.ppcp-has-googlepay-block {
.wp-block-woocommerce-checkout {
.ppcp-button-googlepay {
margin: 0;
height: 48px;
}
}
.wp-block-woocommerce-cart {
.ppcp-button-googlepay {
margin: 0;
height: 48px;
}
}
}
.wp-admin {
.ppcp-button-googlepay {
pointer-events: none;
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
.gpay-button {
min-width: 0 !important;
}
}

View file

@ -3,10 +3,13 @@ import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hidin
import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import UpdatePaymentData from "./Helper/UpdatePaymentData";
import {apmButtonsInit} from "../../../ppcp-button/resources/js/modules/Helper/ApmButtons";
class GooglepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) {
apmButtonsInit(ppcpConfig);
this.isInitialized = false;
this.context = context;

View file

@ -67,7 +67,7 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig.button.wrapper = selector;
applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-googlepay"></div>`;
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-googlepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);

View file

@ -24,13 +24,6 @@ const GooglePayComponent = () => {
manager.init();
};
useEffect(() => {
const bodyClass = 'ppcp-has-googlepay-block';
if (!document.body.classList.contains(bodyClass)) {
document.body.classList.add(bodyClass);
}
}, []);
useEffect(() => {
// Load GooglePay SDK
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
@ -51,7 +44,7 @@ const GooglePayComponent = () => {
}, [paypalLoaded, googlePayLoaded]);
return (
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-googlepay"></div>
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-apm ppcp-button-googlepay"></div>
);
}

View file

@ -311,7 +311,7 @@ class Button implements ButtonInterface {
add_action(
$render_placeholder,
function () {
echo '<span id="ppc-button-googlepay-container-minicart" class="ppcp-button-googlepay ppcp-button-minicart"></span>';
echo '<span id="ppc-button-googlepay-container-minicart" class="ppcp-button-apm ppcp-button-googlepay ppcp-button-minicart"></span>';
},
21
);
@ -325,7 +325,7 @@ class Button implements ButtonInterface {
*/
private function googlepay_button(): void {
?>
<div id="ppc-button-googlepay-container" class="ppcp-button-googlepay">
<div id="ppc-button-googlepay-container" class="ppcp-button-apm ppcp-button-googlepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div>
<?php

View file

@ -1,6 +1,7 @@
<?php
/**
* Properties of the GooglePay module.
* ApmApplies helper.
* Checks if GooglePay is available for a given country and currency.
*
* @package WooCommerce\PayPalCommerce\Googlepay\Helper
*/
@ -15,7 +16,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for DCC.
* The matrix which countries and currency combinations can be used for GooglePay.
*
* @var array
*/
@ -38,7 +39,7 @@ class ApmApplies {
/**
* DccApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for GooglePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,8 @@
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
"core-js": "^3.25.0",
"@paypal/paypal-js": "^6.0.0"
},
"devDependencies": {
"@babel/core": "^7.19",

View file

@ -3,59 +3,17 @@ import {
ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState";
import {loadScript} from "@paypal/paypal-js";
import {
setVisible,
setVisibleByClass
} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import {loadPaypalJsScript} from "../../../ppcp-button/resources/js/modules/Helper/ScriptLoading";
import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler";
import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper";
loadPaypalJsScript(
{
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
},
{
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
})
})
const result = await response.json()
if(result.data.id) {
return result.data.id
}
},
onApprove: async ({ vaultSetupToken }) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
})
})
const result = await response.json()
console.log(result)
},
onError: (error) => {
console.error(error)
}
},
`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const init = () => {
@ -69,6 +27,156 @@ document.addEventListener(
jQuery(document.body).on('click init_add_payment_method', '.payment_methods input.input-radio', function () {
init()
});
setTimeout(() => {
loadScript({
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
components: 'buttons,card-fields',
})
.then((paypal) => {
errorHandler.clear();
paypal.Buttons(
{
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
})
})
const result = await response.json()
if (result.data.id) {
return result.data.id
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
})
})
const result = await response.json();
if(result.success === true) {
window.location.href = ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onError: (error) => {
console.error(error)
errorHandler.message(ppcp_add_payment_method.error_message);
}
},
).render(`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`);
const cardField = paypal.CardFields({
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
payment_method: PaymentMethods.CARDS,
verification_method: ppcp_add_payment_method.verification_method
})
})
const result = await response.json()
if (result.data.id) {
return result.data.id
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
payment_method: PaymentMethods.CARDS
})
})
const result = await response.json();
if(result.success === true) {
window.location.href = ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onError: (error) => {
console.error(error)
errorHandler.message(ppcp_add_payment_method.error_message);
}
});
if (cardField.isEligible()) {
const nameField = document.getElementById('ppcp-credit-card-gateway-card-name');
if (nameField) {
let styles = cardFieldStyles(nameField);
cardField.NameField({style: {'input': styles}}).render(nameField.parentNode);
nameField.hidden = true;
}
const numberField = document.getElementById('ppcp-credit-card-gateway-card-number');
if (numberField) {
let styles = cardFieldStyles(numberField);
cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode);
numberField.hidden = true;
}
const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry');
if (expiryField) {
let styles = cardFieldStyles(expiryField);
cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode);
expiryField.hidden = true;
}
const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc');
if (cvvField) {
let styles = cardFieldStyles(cvvField);
cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode);
cvvField.hidden = true;
}
}
document.querySelector('#place_order').addEventListener("click", (event) => {
event.preventDefault();
cardField.submit()
.catch((error) => {
console.error(error)
});
});
})
}, 1000)
}
);

View file

@ -15,6 +15,8 @@ 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;
/**
* Class CreatePaymentToken
@ -98,16 +100,37 @@ class CreatePaymentToken implements EndpointInterface {
if ( is_user_logged_in() && isset( $result->customer->id ) ) {
update_user_meta( get_current_user_id(), '_ppcp_target_customer_id', $result->customer->id );
$email = '';
if ( isset( $result->payment_source->paypal->email_address ) ) {
$email = $result->payment_source->paypal->email_address;
if ( isset( $result->payment_source->paypal ) ) {
$email = '';
if ( isset( $result->payment_source->paypal->email_address ) ) {
$email = $result->payment_source->paypal->email_address;
}
$this->wc_payment_tokens->create_payment_token_paypal(
get_current_user_id(),
$result->id,
$email
);
}
$this->wc_payment_tokens->create_payment_token_paypal(
get_current_user_id(),
$result->id,
$email
);
if ( isset( $result->payment_source->card ) ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $result->id );
$token->set_user_id( get_current_user_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $result->payment_source->card->last_digits ?? '' );
$expiry = explode( '-', $result->payment_source->card->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$brand = $result->payment_source->card->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
if ( $brand ) {
$token->set_card_type( $brand );
}
$token->save();
}
}
wp_send_json_success( $result );

View file

@ -14,6 +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\WcGateway\Gateway\CreditCardGateway;
/**
* Class CreateSetupToken
@ -67,13 +68,8 @@ class CreateSetupToken implements EndpointInterface {
*/
public function handle_request(): bool {
try {
$this->request_data->read_request( $this->nonce() );
$data = $this->request_data->read_request( $this->nonce() );
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$payment_source = new PaymentSource(
'paypal',
(object) array(
@ -85,6 +81,28 @@ class CreateSetupToken implements EndpointInterface {
)
);
$payment_method = $data['payment_method'] ?? '';
if ( $payment_method === CreditCardGateway::ID ) {
$properties = (object) array();
$verification_method = $data['verification_method'] ?? '';
if ( $verification_method === 'SCA_WHEN_REQUIRED' || $verification_method === 'SCA_ALWAYS' ) {
$properties = (object) array(
'verification_method' => $verification_method,
'usage_type' => 'MERCHANT',
'experience_context' => (object) array(
'return_url' => esc_url( wc_get_account_endpoint_url( 'payment-methods' ) ),
'cancel_url' => esc_url( wc_get_account_endpoint_url( 'add-payment-method' ) ),
),
);
}
$payment_source = new PaymentSource(
'card',
$properties
);
}
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source );
wp_send_json_success( $result );

View file

@ -28,6 +28,7 @@ use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class SavePaymentMethodsModule
@ -218,14 +219,21 @@ class SavePaymentMethodsModule implements ModuleInterface {
$id_token = $api->id_token( $target_customer_id );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$verification_method = $settings->has( '3d_secure_contingency' ) ? $settings->get( '3d_secure_contingency' ) : '';
wp_localize_script(
'ppcp-add-payment-method',
'ppcp_add_payment_method',
array(
'client_id' => $c->get( 'button.client_id' ),
'merchant_id' => $c->get( 'api.merchant_id' ),
'id_token' => $id_token,
'ajax' => array(
'client_id' => $c->get( 'button.client_id' ),
'merchant_id' => $c->get( 'api.merchant_id' ),
'id_token' => $id_token,
'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ),
'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ),
'verification_method' => $verification_method,
'ajax' => array(
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),

View file

@ -1031,6 +1031,13 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@paypal/paypal-js@^6.0.0":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-6.0.1.tgz#5d68d5863a5176383fee9424bc944231668fcffd"
integrity sha512-bvYetmkg2GEC6onsUJQx1E9hdAJWff2bS3IPeiZ9Sh9U7h26/fIgMKm240cq/908sbSoDjHys75XXd8at9OpQA==
dependencies:
promise-polyfill "^8.3.0"
"@types/eslint-scope@^3.7.3":
version "3.7.5"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.5.tgz#e28b09dbb1d9d35fdfa8a884225f00440dfc5a3e"
@ -1868,6 +1875,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
promise-polyfill@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
punycode@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"

View file

@ -1,3 +1,4 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
.ppcp-field-hidden {
display: none !important;
@ -15,3 +16,12 @@
padding-left: 20px;
}
}
// Prevents spacing after button group.
.ppcp-button-preview-inner {
line-height: 0;
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -15,9 +15,6 @@ document.addEventListener(
jQuery( '*[data-ppcp-display]' ).each( (index, el) => {
const rules = jQuery(el).data('ppcpDisplay');
// console.log('rules', rules);
for (const rule of rules) {
displayManager.addRule(rule);
}

View file

@ -353,5 +353,41 @@ document.addEventListener(
}, 'card'));
});
}
// Logic to handle the "Check available features" button.
((props) => {
const $btn = jQuery(props.button);
$btn.click(async () => {
$btn.prop('disabled', true);
const response = await fetch(
props.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(
{
nonce: props.nonce,
}
)
}
);
const responseData = await response.json();
if (!responseData.success) {
alert(responseData.data.message);
$btn.prop('disabled', false);
} else {
window.location.reload();
}
});
})(PayPalCommerceGatewaySettings.ajax.refresh_feature_status);
}
);

View file

@ -21,6 +21,7 @@ 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\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
@ -1003,6 +1004,13 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.endpoint.refresh-feature-status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
return new RefreshFeatureStatusEndpoint(
$container->get( 'wcgateway.settings' ),
new Cache( 'ppcp-timeout' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';

View file

@ -10,9 +10,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
/**
* Class SettingsPageAssets
@ -237,6 +239,13 @@ class SettingsPageAssets {
'disabled_sources' => $this->disabled_sources,
'all_funding_sources' => $this->all_funding_sources,
'components' => array( 'buttons', 'funding-eligibility', 'messages' ),
'ajax' => array(
'refresh_feature_status' => array(
'endpoint' => \WC_AJAX::get_endpoint( RefreshFeatureStatusEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( RefreshFeatureStatusEndpoint::nonce() ),
'button' => '.ppcp-refresh-feature-status',
),
),
)
)
);

View file

@ -0,0 +1,115 @@
<?php
/**
* Controls the endpoint for refreshing feature status.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class RefreshFeatureStatusEndpoint
*/
class RefreshFeatureStatusEndpoint {
const ENDPOINT = 'ppc-refresh-feature-status';
const CACHE_KEY = 'refresh_feature_status_timeout';
const TIMEOUT = 60;
/**
* The settings.
*
* @var ContainerInterface
*/
protected $settings;
/**
* The cache.
*
* @var Cache
*/
protected $cache;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* RefreshFeatureStatusEndpoint constructor.
*
* @param ContainerInterface $settings The settings.
* @param Cache $cache The cache.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
ContainerInterface $settings,
Cache $cache,
LoggerInterface $logger
) {
$this->settings = $settings;
$this->cache = $cache;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request(): void {
$now = time();
$last_request_time = $this->cache->get( self::CACHE_KEY ) ?: 0;
$seconds_missing = $last_request_time + self::TIMEOUT - $now;
if ( ! $this->verify_nonce() ) {
wp_send_json_error(
array(
'message' => __( 'Expired request.', 'woocommerce-paypal-payments' ),
)
);
}
if ( $seconds_missing > 0 ) {
wp_send_json_error(
array(
'message' => sprintf(
// translators: %1$s is the number of seconds remaining.
__( 'Wait %1$s seconds before trying again.', 'woocommerce-paypal-payments' ),
$seconds_missing
),
)
);
}
$this->cache->set( self::CACHE_KEY, $now, self::TIMEOUT );
do_action( 'woocommerce_paypal_payments_clear_apm_product_status', $this->settings );
wp_send_json_success();
}
/**
* Verifies the nonce.
*
* @return bool
*/
private function verify_nonce(): bool {
$json = json_decode( file_get_contents( 'php://input' ) ?: '', true );
return wp_verify_nonce( $json['nonce'] ?? '', self::nonce() ) !== false;
}
}

View file

@ -390,6 +390,16 @@ return function ( ContainerInterface $container, array $fields ): array {
'</a>'
),
),
'refresh_feature_status' => array(
'title' => __( 'Refresh feature availability status', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => '<button type="button" class="button ppcp-refresh-feature-status">' . esc_html__( 'Check available features', 'woocommerce-paypal-payments' ) . '</button>',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
),
'ppcp_dcc_status' => array(
'title' => __( 'Advanced Credit and Debit Card Payments', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',

View file

@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
@ -255,6 +256,16 @@ class WCGatewayModule implements ModuleInterface {
}
);
add_action(
'wc_ajax_' . RefreshFeatureStatusEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'wcgateway.endpoint.refresh-feature-status' );
assert( $endpoint instanceof RefreshFeatureStatusEndpoint );
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_gateway_migrate',
static function () use ( $c ) {

View file

@ -11,7 +11,9 @@ namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WC_Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
@ -21,6 +23,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
@ -193,6 +196,54 @@ class RenewalHandler {
$token = $this->get_token_for_customer( $customer, $wc_order );
if ( $token ) {
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$stored_credentials = array(
'payment_initiator' => 'MERCHANT',
'payment_type' => 'RECURRING',
'usage' => 'SUBSEQUENT',
);
$subscriptions = wcs_get_subscriptions_for_renewal_order( $wc_order );
foreach ( $subscriptions as $post_id => $subscription ) {
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
if ( $previous_transaction_reference ) {
$stored_credentials['previous_transaction_reference'] = $previous_transaction_reference;
break;
}
}
$payment_source = new PaymentSource(
'card',
(object) array(
'vault_id' => $token->id(),
'stored_credential' => $stored_credentials,
)
);
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
$payer,
null,
'',
ApplicationContext::USER_ACTION_CONTINUE,
'',
array(),
$payment_source
);
$this->handle_paypal_order( $wc_order, $order );
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
return;
}
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,

View file

@ -142,48 +142,6 @@ class WcSubscriptionsModule implements ModuleInterface {
20,
2
);
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
if ( ! $subscription_id ) {
return $data;
}
$subscription = wc_get_order( $subscription_id );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $data;
}
if (
$wc_order_action === 'wcs_process_renewal' && $subscription->get_payment_method() === CreditCardGateway::ID
&& isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN'
&& isset( $data['payment_source']['token']['source']->card )
) {
$data['payment_source'] = array(
'card' => array(
'vault_id' => $data['payment_source']['token']['id'],
'stored_credential' => array(
'payment_initiator' => 'MERCHANT',
'payment_type' => 'RECURRING',
'usage' => 'SUBSEQUENT',
),
),
);
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
if ( $previous_transaction_reference ) {
$data['payment_source']['card']['stored_credential']['previous_transaction_reference'] = $previous_transaction_reference;
}
}
return $data;
}
);
}
/**

View file

@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
"version": "2.4.2",
"version": "2.4.3",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",

View file

@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, checkout, cart, pay later, apple
Requires at least: 5.3
Tested up to: 6.4
Requires PHP: 7.2
Stable tag: 2.4.2
Stable tag: 2.4.3
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -179,6 +179,19 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
= 2.4.3 - xxxx-xx-xx =
* Fix - PayPal Subscription initiated without a WooCommerce order #1907
* Fix - Block Checkout reloads when submitting order with empty fields #1904
* Fix - "Send checkout billing and shipping data to Apple Pay" displayed when Apple Pay is disabled #1883
* Fix - "Order does not contain intent" error for ACDC renewals when triggering 3D Secure #1888
* Enhancement - Add button to reload feature eligibility status from Connection tab #1902
* Enhancement - Apple Pay validation message improvements #1901
* Enhancement - Improve support for Classic Cart & Classic Checkout blocks #1894
* Enhancement - Ensure uniform button appearance for PayPal, Google Pay, and Apple Pay buttons #1900
* Enhancement - remove string translations for package tracking carriers from repository #1885
* Enhancement - Incorrect margins when PayPal buttons are rendered as separate gateways. #1908
* Feature preview - Save payment methods (Vault v3) integration #1779
= 2.4.2 - 2023-12-04 =
* Fix - Action callback arguments count in ShipStation tracking integration #1841
* Fix - Google Pay scripts loading on unrelated admin pages #1834

View file

@ -116,6 +116,9 @@ class RenewalHandlerTest extends TestCase
->shouldReceive('payment_source')
->andReturn(null);
$wcOrder
->shouldReceive('get_payment_method')
->andReturn('');
$wcOrder
->shouldReceive('get_meta')
->andReturn('');

View file

@ -3,7 +3,7 @@
* 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.4.2
* Version: 2.4.3
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
@ -25,7 +25,7 @@ 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', '2023-11-28' );
define( 'PAYPAL_INTEGRATION_DATE', '2023-12-18' );
! 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_' );