Merge pull request #1858 from woocommerce/PCP-2273-block-buttons

Render block buttons separately and add block style settings
This commit is contained in:
Niklas Gutberlet 2023-11-24 14:22:24 +01:00 committed by GitHub
commit 9f5d62e2f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 506 additions and 137 deletions

View file

@ -32,7 +32,7 @@
.wp-block-woocommerce-checkout { .wp-block-woocommerce-checkout {
#applepay-container, .ppcp-button-applepay { #applepay-container, .ppcp-button-applepay {
--apple-pay-button-margin: 0; --apple-pay-button-margin: 0;
--apple-pay-button-height: 40px; --apple-pay-button-height: 48px;
&.ppcp-button-pill { &.ppcp-button-pill {
--apple-pay-button-border-radius: 50px; --apple-pay-button-border-radius: 50px;
} }
@ -42,7 +42,7 @@
.wp-block-woocommerce-cart { .wp-block-woocommerce-cart {
#applepay-container, .ppcp-button-applepay { #applepay-container, .ppcp-button-applepay {
--apple-pay-button-margin: 0; --apple-pay-button-margin: 0;
--apple-pay-button-height: 40px; --apple-pay-button-height: 48px;
} }
/* Workaround for blocks grid */ /* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons { .wc-block-components-express-payment__event-buttons {

View file

@ -107,7 +107,7 @@ class ApplepayButton {
let config = { let config = {
wrapper: this.buttonConfig.button.wrapper, wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style, ppcpStyle: this.ppcpConfig.button.style,
//buttonStyle: this.buttonConfig.button.style, buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper ppcpButtonWrapper: this.ppcpConfig.button.wrapper
} }
@ -119,7 +119,7 @@ class ApplepayButton {
} }
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) { if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway'; config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal';
} }
return config; return config;
@ -167,14 +167,8 @@ class ApplepayButton {
addButton() { addButton() {
this.log('addButton', this.context); this.log('addButton', this.context);
const wrapper = const { wrapper, ppcpStyle } = this.contextConfig();
(this.context === 'mini-cart')
? this.buttonConfig.button.mini_cart_wrapper
: this.buttonConfig.button.wrapper;
const shape =
(this.context === 'mini-cart')
? this.ppcpConfig.button.mini_cart_style.shape
: this.ppcpConfig.button.style.shape;
const appleContainer = document.getElementById(wrapper); const appleContainer = document.getElementById(wrapper);
const type = this.buttonConfig.button.type; const type = this.buttonConfig.button.type;
const language = this.buttonConfig.button.lang; const language = this.buttonConfig.button.lang;
@ -185,7 +179,12 @@ class ApplepayButton {
appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`; appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`;
} }
jQuery('#' + wrapper).addClass('ppcp-button-' + shape); jQuery('#' + wrapper).addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
jQuery('#' + wrapper).css('--apple-pay-button-height', `${ppcpStyle.height}px`)
}
jQuery(wrapper).append(appleContainer); jQuery(wrapper).append(appleContainer);
} }

View file

@ -55,9 +55,8 @@ const ApplePayComponent = () => {
} }
const features = ['products']; const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
registerMethod({ registerExpressPaymentMethod({
name: buttonData.id, name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>, label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,
content: <ApplePayComponent isEditing={false}/>, content: <ApplePayComponent isEditing={false}/>,

View file

@ -1,13 +1,20 @@
import {useEffect, useState} from '@wordpress/element'; import {useEffect, useState} from '@wordpress/element';
import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry';
import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address";
import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' import {
loadPaypalScriptPromise
} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import {
normalizeStyleForFundingSource
} from '../../../ppcp-button/resources/js/modules/Helper/Style'
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
const config = wc.wcSettings.getSetting('ppcp-gateway_data'); const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource; window.ppcpFundingSource = config.fundingSource;
let registeredContext = false;
const PayPalComponent = ({ const PayPalComponent = ({
onClick, onClick,
onClose, onClose,
@ -18,12 +25,15 @@ const PayPalComponent = ({
activePaymentMethod, activePaymentMethod,
shippingData, shippingData,
isEditing, isEditing,
fundingSource,
}) => { }) => {
const {onPaymentSetup, onCheckoutFail, onCheckoutValidation} = eventRegistration; const {onPaymentSetup, onCheckoutFail, onCheckoutValidation} = eventRegistration;
const {responseTypes} = emitResponse; const {responseTypes} = emitResponse;
const [paypalOrder, setPaypalOrder] = useState(null); const [paypalOrder, setPaypalOrder] = useState(null);
const methodId = fundingSource ? `${config.id}-${fundingSource}` : config.id;
useEffect(() => { useEffect(() => {
// fill the form if in continuation (for product or mini-cart buttons) // fill the form if in continuation (for product or mini-cart buttons)
if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) { if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) {
@ -40,24 +50,6 @@ const PayPalComponent = ({
window.ppcpContinuationFilled = true; window.ppcpContinuationFilled = true;
}, []) }, [])
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (!loaded && !config.scriptData.continuation) {
loadPaypalScript(config.scriptData, () => {
setLoaded(true);
buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, {
createOrder: () => {
return createOrder();
},
onApprove: (data, actions) => {
return handleApprove(data, actions);
},
});
});
}
}, [loaded]);
const createOrder = async () => { const createOrder = async () => {
try { try {
const res = await fetch(config.scriptData.ajax.create_order.endpoint, { const res = await fetch(config.scriptData.ajax.create_order.endpoint, {
@ -233,7 +225,7 @@ const PayPalComponent = ({
} }
useEffect(() => { useEffect(() => {
if (activePaymentMethod !== config.id) { if (activePaymentMethod !== methodId) {
return; return;
} }
@ -269,7 +261,7 @@ const PayPalComponent = ({
}, [onPaymentSetup, paypalOrder, activePaymentMethod]); }, [onPaymentSetup, paypalOrder, activePaymentMethod]);
useEffect(() => { useEffect(() => {
if (activePaymentMethod !== config.id) { if (activePaymentMethod !== methodId) {
return; return;
} }
const unsubscribe = onCheckoutFail(({ processingResponse }) => { const unsubscribe = onCheckoutFail(({ processingResponse }) => {
@ -296,15 +288,26 @@ const PayPalComponent = ({
) )
} }
if (!loaded) { if (!registeredContext) {
return null; buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, {
createOrder: () => {
return createOrder();
},
onApprove: (data, actions) => {
return handleApprove(data, actions);
},
});
registeredContext = true;
} }
const style = normalizeStyleForFundingSource(config.scriptData.button.style, fundingSource);
const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM }); const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM });
return ( return (
<PayPalButton <PayPalButton
style={config.scriptData.button.style} fundingSource={fundingSource}
style={style}
onClick={handleClick} onClick={handleClick}
onCancel={onClose} onCancel={onClose}
onError={onClose} onError={onClose}
@ -317,7 +320,7 @@ const PayPalComponent = ({
const features = ['products']; const features = ['products'];
if (config.usePlaceOrder && !config.scriptData.continuation) { if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.continuation) {
registerPaymentMethod({ registerPaymentMethod({
name: config.id, name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>, label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
@ -330,22 +333,39 @@ if (config.usePlaceOrder && !config.scriptData.continuation) {
features: features, features: features,
}, },
}); });
} else {
let registerMethod = registerExpressPaymentMethod;
if (config.scriptData.continuation) {
features.push('ppcp_continuation');
registerMethod = registerPaymentMethod;
} }
registerMethod({ if (config.scriptData.continuation) {
registerPaymentMethod({
name: config.id, name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>, label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false}/>, content: <PayPalComponent isEditing={false}/>,
edit: <PayPalComponent isEditing={true}/>, edit: <PayPalComponent isEditing={true}/>,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => config.enabled, canMakePayment: () => true,
supports: {
features: [...features, 'ppcp_continuation'],
},
});
} else if (!config.usePlaceOrder) {
const paypalScriptPromise = loadPaypalScriptPromise(config.scriptData);
for (const fundingSource of ['paypal', ...config.enabledFundingSources]) {
registerExpressPaymentMethod({
name: `${config.id}-${fundingSource}`,
paymentMethodId: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false} fundingSource={fundingSource}/>,
edit: <PayPalComponent isEditing={true} fundingSource={fundingSource}/>,
ariaLabel: config.title,
canMakePayment: async () => {
await paypalScriptPromise;
return paypal.Buttons({fundingSource}).isEligible();
},
supports: { supports: {
features: features, features: features,
}, },
}); });
} }
}

View file

@ -35,8 +35,10 @@ return array(
$container->get( 'blocks.settings.final_review_enabled' ), $container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'session.cancellation.view' ), $container->get( 'session.cancellation.view' ),
$container->get( 'session.handler' ), $container->get( 'session.handler' ),
$container->get( 'blocks.add-place-order-method' ),
$container->get( 'wcgateway.use-place-order-button' ), $container->get( 'wcgateway.use-place-order-button' ),
$container->get( 'wcgateway.place-order-button-text' ) $container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'wcgateway.all-funding-sources' )
); );
}, },
'blocks.settings.final_review_enabled' => static function ( ContainerInterface $container ): bool { 'blocks.settings.final_review_enabled' => static function ( ContainerInterface $container ): bool {
@ -56,4 +58,14 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'blocks.add-place-order-method' => function ( ContainerInterface $container ) : bool {
/**
* Whether to create a non-express method with the standard "Place order" button redirecting to PayPal.
*/
return apply_filters(
'woocommerce_paypal_payments_blocks_add_place_order_method',
true
);
},
); );

View file

@ -88,7 +88,14 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
private $session_handler; private $session_handler;
/** /**
* Whether to use the standard "Place order" button. * Whether to create a non-express method with the standard "Place order" button.
*
* @var bool
*/
protected $add_place_order_method;
/**
* Whether to use the standard "Place order" button instead of PayPal buttons.
* *
* @var bool * @var bool
*/ */
@ -101,6 +108,13 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/ */
protected $place_order_button_text; protected $place_order_button_text;
/**
* All existing funding sources for PayPal buttons.
*
* @var array
*/
private $all_funding_sources;
/** /**
* Assets constructor. * Assets constructor.
* *
@ -113,8 +127,10 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* @param bool $final_review_enabled Whether the final review is enabled. * @param bool $final_review_enabled Whether the final review is enabled.
* @param CancelView $cancellation_view The cancellation view. * @param CancelView $cancellation_view The cancellation view.
* @param SessionHandler $session_handler The Session handler. * @param SessionHandler $session_handler The Session handler.
* @param bool $use_place_order Whether to use the standard "Place order" button. * @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button.
* @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons.
* @param string $place_order_button_text The text for the standard "Place order" button. * @param string $place_order_button_text The text for the standard "Place order" button.
* @param array $all_funding_sources All existing funding sources for PayPal buttons.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
@ -126,8 +142,10 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
bool $final_review_enabled, bool $final_review_enabled,
CancelView $cancellation_view, CancelView $cancellation_view,
SessionHandler $session_handler, SessionHandler $session_handler,
bool $add_place_order_method,
bool $use_place_order, bool $use_place_order,
string $place_order_button_text string $place_order_button_text,
array $all_funding_sources
) { ) {
$this->name = PayPalGateway::ID; $this->name = PayPalGateway::ID;
$this->module_url = $module_url; $this->module_url = $module_url;
@ -139,8 +157,10 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
$this->final_review_enabled = $final_review_enabled; $this->final_review_enabled = $final_review_enabled;
$this->cancellation_view = $cancellation_view; $this->cancellation_view = $cancellation_view;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->add_place_order_method = $add_place_order_method;
$this->use_place_order = $use_place_order; $this->use_place_order = $use_place_order;
$this->place_order_button_text = $place_order_button_text; $this->place_order_button_text = $place_order_button_text;
$this->all_funding_sources = $all_funding_sources;
} }
/** /**
@ -194,6 +214,14 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
} }
} }
$disabled_funding_sources = explode( ',', $script_data['url_params']['disable-funding'] ) ?: array();
$funding_sources = array_values(
array_diff(
array_keys( $this->all_funding_sources ),
$disabled_funding_sources
)
);
return array( return array(
'id' => $this->gateway->id, 'id' => $this->gateway->id,
'title' => $this->gateway->title, 'title' => $this->gateway->title,
@ -201,8 +229,10 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ), 'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ),
'fundingSource' => $this->session_handler->funding_source(), 'fundingSource' => $this->session_handler->funding_source(),
'finalReviewEnabled' => $this->final_review_enabled, 'finalReviewEnabled' => $this->final_review_enabled,
'addPlaceOrderMethod' => $this->add_place_order_method,
'usePlaceOrder' => $this->use_place_order, 'usePlaceOrder' => $this->use_place_order,
'placeOrderButtonText' => $this->place_order_button_text, 'placeOrderButtonText' => $this->place_order_button_text,
'enabledFundingSources' => $funding_sources,
'ajax' => array( 'ajax' => array(
'update_shipping' => array( 'update_shipping' => array(
'endpoint' => WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ), 'endpoint' => WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ),

View file

@ -279,11 +279,12 @@ document.addEventListener(
}); });
let bootstrapped = false; let bootstrapped = false;
let failed = false;
hideOrderButtonIfPpcpGateway(); hideOrderButtonIfPpcpGateway();
jQuery(document.body).on('updated_checkout payment_method_selected', () => { jQuery(document.body).on('updated_checkout payment_method_selected', () => {
if (bootstrapped) { if (bootstrapped || failed) {
return; return;
} }
@ -294,6 +295,12 @@ document.addEventListener(
bootstrapped = true; bootstrapped = true;
bootstrap(); bootstrap();
}, () => {
failed = true;
setVisibleByClass(ORDER_BUTTON_SELECTOR, true, 'ppcp-hidden');
buttonsSpinner.unblock();
cardsSpinner.unblock();
}); });
}, },
); );

View file

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

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. // This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || { let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
isLoading: false, 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 PayPal is already loaded call the onLoaded callback and return.
if (typeof paypal !== 'undefined') { if (typeof paypal !== 'undefined') {
onLoaded(); onLoaded();
@ -19,6 +20,9 @@ export const loadPaypalScript = (config, onLoaded) => {
// Add the onLoaded callback to the onLoadedCallbacks stack. // Add the onLoaded callback to the onLoadedCallbacks stack.
options.onLoadedCallbacks.push(onLoaded); options.onLoadedCallbacks.push(onLoaded);
if (onError) {
options.onErrorCallbacks.push(onError);
}
// Return if it's still loading. // Return if it's still loading.
if (options.isLoading) { if (options.isLoading) {
@ -26,6 +30,12 @@ export const loadPaypalScript = (config, onLoaded) => {
} }
options.isLoading = true; options.isLoading = true;
const resetState = () => {
options.isLoading = false;
options.onLoadedCallbacks = [];
options.onErrorCallbacks = [];
}
// Callback to be called once the PayPal script is loaded. // Callback to be called once the PayPal script is loaded.
const callback = (paypal) => { const callback = (paypal) => {
widgetBuilder.setPaypal(paypal); widgetBuilder.setPaypal(paypal);
@ -34,8 +44,14 @@ export const loadPaypalScript = (config, onLoaded) => {
onLoadedCallback(); onLoadedCallback();
} }
options.isLoading = false; resetState();
options.onLoadedCallbacks = []; }
const errorCallback = (err) => {
for (const onErrorCallback of options.onErrorCallbacks) {
onErrorCallback(err);
}
resetState();
} }
// Build the PayPal script options. // Build the PayPal script options.
@ -44,12 +60,20 @@ export const loadPaypalScript = (config, onLoaded) => {
// Load PayPal script for special case with data-client-token // Load PayPal script for special case with data-client-token
if (config.data_client_id.set_attribute) { if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback); dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);
return; return;
} }
// Load PayPal script // 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) => { export const loadPaypalJsScript = (options, buttons, container) => {

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

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

View file

@ -1049,30 +1049,39 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'mini_cart_wrapper' => '#ppc-button-minicart', 'mini_cart_wrapper' => '#ppc-button-minicart',
'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ), 'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ),
'cancel_wrapper' => '#ppcp-cancel', 'cancel_wrapper' => '#ppcp-cancel',
'mini_cart_style' => array( 'mini_cart_style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ), 'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ), 'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ), 'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
'label' => $this->style_for_context( 'label', 'mini-cart' ), 'label' => $this->style_for_context( 'label', 'mini-cart' ),
'tagline' => $this->style_for_context( 'tagline', '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, 'height' => $this->normalize_height( $this->style_for_context( 'height', 'mini-cart', 35 ), 25, 55 ),
)
), ),
'style' => array( 'style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', $this->context() ), 'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ), 'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ), 'shape' => $this->style_for_context( 'shape', $this->context() ),
'label' => $this->style_for_context( 'label', $this->context() ), 'label' => $this->style_for_context( 'label', $this->context() ),
'tagline' => $this->style_for_context( 'tagline', $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( 'separate_buttons' => array(
'card' => array( 'card' => array(
'id' => CardButtonGateway::ID, 'id' => CardButtonGateway::ID,
'wrapper' => '#ppc-button-' . CardButtonGateway::ID, 'wrapper' => '#ppc-button-' . CardButtonGateway::ID,
'style' => array( 'style' => $this->normalize_style(
array(
'shape' => $this->style_for_apm( 'shape', 'card' ), 'shape' => $this->style_for_apm( 'shape', 'card' ),
'color' => $this->style_for_apm( 'color', 'card', 'black' ), 'color' => $this->style_for_apm( 'color', 'card', 'black' ),
'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal', 'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal',
)
), ),
), ),
), ),
@ -1143,13 +1152,6 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$localize['pay_now'] = $this->pay_now_script_data(); $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() ) { if ( $this->is_paypal_continuation() ) {
$order = $this->session_handler->order(); $order = $this->session_handler->order();
assert( $order !== null ); assert( $order !== null );
@ -1410,12 +1412,14 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* *
* @param string $style The name of the style property. * @param string $style The name of the style property.
* @param string $context The context. * @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 { private function style_for_context( string $style, string $context, $default = null ) {
// Use the cart/checkout styles for blocks. if ( $context === 'checkout-block' ) {
$context = str_replace( '-block', '', $context ); $context = 'checkout-block-express';
}
$defaults = array( $defaults = array(
'layout' => 'vertical', 'layout' => 'vertical',
@ -1433,6 +1437,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->get_style_value( "button_{$context}_${style}" ) return $this->get_style_value( "button_{$context}_${style}" )
?? $this->get_style_value( "button_${style}" ) ?? $this->get_style_value( "button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->normalize_style_value( $defaults[ $style ] ?? '' ); ?? $this->normalize_style_value( $defaults[ $style ] ?? '' );
} }
@ -1443,9 +1448,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* @param string $apm The APM name, such as 'card'. * @param string $apm The APM name, such as 'card'.
* @param ?mixed $default The default value. * @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}" ) return $this->get_style_value( "${apm}_button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null ) ?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->style_for_context( $style, 'checkout' ); ?? $this->style_for_context( $style, 'checkout' );
@ -1455,9 +1460,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Returns the style property value or null. * Returns the style property value or null.
* *
* @param string $key The style property key in the settings. * @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 ) ) { if ( ! $this->settings->has( $key ) ) {
return null; return null;
} }
@ -1468,27 +1473,49 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Converts the style property value to string. * Converts the style property value to string.
* *
* @param mixed $value The style property value. * @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 ) ) { if ( is_bool( $value ) ) {
$value = $value ? 'true' : 'false'; $value = $value ? 'true' : 'false';
} }
if ( is_int( $value ) ) {
return $value;
}
return (string) $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. * @return int The normalized output value.
*/ */
private function normalize_height( int $height ): int { private function normalize_height( $height, int $min, int $max ): int {
if ( $height < 25 ) { $height = (int) $height;
return 25; if ( $height < $min ) {
return $min;
} }
if ( $height > 55 ) { if ( $height > $max ) {
return 55; return $max;
} }
return $height; return $height;

View file

@ -25,14 +25,14 @@
.wp-block-woocommerce-checkout { .wp-block-woocommerce-checkout {
.ppcp-button-googlepay { .ppcp-button-googlepay {
margin: 0; margin: 0;
height: 40px; height: 48px;
} }
} }
.wp-block-woocommerce-cart { .wp-block-woocommerce-cart {
.ppcp-button-googlepay { .ppcp-button-googlepay {
margin: 0; margin: 0;
height: 40px; height: 48px;
} }
/* Workaround for blocks grid */ /* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons { .wc-block-components-express-payment__event-buttons {

View file

@ -111,7 +111,7 @@ class GooglepayButton {
} }
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) { if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway'; config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal';
} }
return config; return config;
@ -168,6 +168,10 @@ class GooglepayButton {
this.waitForWrapper(wrapper, () => { this.waitForWrapper(wrapper, () => {
jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape); jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
jQuery(wrapper).css('height', `${ppcpStyle.height}px`)
}
const button = const button =
this.paymentsClient.createButton({ this.paymentsClient.createButton({
onClick: this.onButtonClick.bind(this), onClick: this.onButtonClick.bind(this),

View file

@ -56,9 +56,8 @@ const GooglePayComponent = () => {
} }
const features = ['products']; const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
registerMethod({ registerExpressPaymentMethod({
name: buttonData.id, name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>, label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,
content: <GooglePayComponent isEditing={false}/>, content: <GooglePayComponent isEditing={false}/>,

View file

@ -9,7 +9,7 @@ document.addEventListener(
const smartButtonLocationsSelector = '#field-smart_button_locations'; const smartButtonLocationsSelector = '#field-smart_button_locations';
const smartButtonLocationsSelect = smartButtonLocationsSelector + ' select'; const smartButtonLocationsSelect = smartButtonLocationsSelector + ' select';
const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart']; const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express'];
const groupToggle = (selector, group) => { const groupToggle = (selector, group) => {
const toggleElement = document.querySelector(selector); const toggleElement = document.querySelector(selector);
@ -317,6 +317,7 @@ document.addEventListener(
'#field-button' + locationPrefix + '_label', '#field-button' + locationPrefix + '_label',
'#field-button' + locationPrefix + '_color', '#field-button' + locationPrefix + '_color',
'#field-button' + locationPrefix + '_shape', '#field-button' + locationPrefix + '_shape',
'#field-button' + locationPrefix + '_height',
'#field-button' + locationPrefix + '_preview', '#field-button' + locationPrefix + '_preview',
] ]
@ -324,11 +325,7 @@ document.addEventListener(
inputSelectors.push('#field-button_' + location + '_heading'); inputSelectors.push('#field-button_' + location + '_heading');
} }
if (location === 'mini-cart') { return inputSelectors.filter(selector => document.querySelector(selector));
inputSelectors.push('#field-button' + locationPrefix + '_height');
}
return inputSelectors
} }
const allPayLaterMessaginginputSelectors = () => { const allPayLaterMessaginginputSelectors = () => {

View file

@ -310,11 +310,11 @@ document.addEventListener(
loadPaypalScript(oldScriptSettings, () => { loadPaypalScript(oldScriptSettings, () => {
const payLaterMessagingLocations = ['product', 'cart', 'checkout', 'shop', 'home', 'general']; const payLaterMessagingLocations = ['product', 'cart', 'checkout', 'shop', 'home', 'general'];
const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'general']; const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express', 'general'];
paypalButtonLocations.forEach((location) => { paypalButtonLocations.forEach((location) => {
const inputNamePrefix = location === 'checkout' ? '#ppcp-button' : '#ppcp-button_' + location; const inputNamePrefix = location === 'checkout' ? '#ppcp-button' : '#ppcp-button_' + location;
let wrapperName = location.charAt(0).toUpperCase() + location.slice(1); const wrapperName = location.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
const fields = { const fields = {
'color': inputNamePrefix + '_color', 'color': inputNamePrefix + '_color',
'shape': inputNamePrefix + '_shape', 'shape': inputNamePrefix + '_shape',
@ -323,9 +323,8 @@ document.addEventListener(
'layout': inputNamePrefix + '_layout', 'layout': inputNamePrefix + '_layout',
} }
if (location === 'mini-cart') { if (document.querySelector(inputNamePrefix + '_height')) {
fields['height'] = inputNamePrefix + '_height'; fields['height'] = inputNamePrefix + '_height';
wrapperName = 'MiniCart';
} }
createButtonPreview(() => getButtonSettings('#ppcp' + wrapperName + 'ButtonPreview', fields)); createButtonPreview(() => getButtonSettings('#ppcp' + wrapperName + 'ButtonPreview', fields));

View file

@ -802,6 +802,242 @@ return function ( ContainerInterface $container, array $fields ): array {
'requirements' => array(), 'requirements' => array(),
'gateway' => 'paypal', 'gateway' => 'paypal',
), ),
// Block express checkout settings.
'button_checkout-block-express_heading' => array(
'heading' => __( 'Block Express Checkout Buttons', 'woocommerce-paypal-payments' ),
'description' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
'Customize the appearance of the PayPal smart buttons on the %1$sBlock Express Checkout%2$s.',
'woocommerce-paypal-payments'
),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-block-express-checkout" target="_blank">',
'</a>'
),
'type' => 'ppcp-heading',
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_checkout-block-express_label' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
/**
* Returns default label ID of the PayPal button in block express checkout.
*/
'default' => apply_filters( 'woocommerce_paypal_payments_button_checkout_block_express_label_default', 'paypal' ),
'desc_tip' => true,
'description' => __(
'This controls the label on the primary button.',
'woocommerce-paypal-payments'
),
'options' => array(
'paypal' => __( 'PayPal', 'woocommerce-paypal-payments' ),
'checkout' => __( 'Checkout', 'woocommerce-paypal-payments' ),
'buynow' => __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ),
'pay' => __( 'Pay with PayPal', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_checkout-block-express_color' => array(
'title' => __( 'Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'gold',
'desc_tip' => true,
'description' => __(
'Controls the background color of the primary button. Use "Gold" to leverage PayPal\'s recognition and preference, or change it to match your site design or aesthetic.',
'woocommerce-paypal-payments'
),
'options' => array(
'gold' => __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ),
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_checkout-block-express_shape' => array(
'title' => __( 'Shape', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'rect',
'desc_tip' => true,
'description' => __(
'The pill-shaped button\'s unique and powerful shape signifies PayPal in people\'s minds. Use the rectangular button as an alternative when pill-shaped buttons might pose design challenges.',
'woocommerce-paypal-payments'
),
'options' => array(
'pill' => __( 'Pill', 'woocommerce-paypal-payments' ),
'rect' => __( 'Rectangle', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_checkout-block-express_height' => array(
'title' => __( 'Button Height', 'woocommerce-paypal-payments' ),
'type' => 'number',
'default' => '48',
'custom_attributes' => array(
'min' => 40,
'max' => 55,
),
'desc_tip' => true,
'description' => __( 'Set a value from 40 to 55.', 'woocommerce-paypal-payments' ),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_checkout-block-express_preview' => array(
'type' => 'ppcp-text',
'text' => $render_preview_element( 'ppcpCheckoutBlockExpressButtonPreview' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
// Block cart settings.
'button_cart-block_heading' => array(
'heading' => __( 'Block Cart Buttons', 'woocommerce-paypal-payments' ),
'description' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
'Customize the appearance of the PayPal smart buttons on the %1$sBlock Cart%2$s.',
'woocommerce-paypal-payments'
),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-cart-block" target="_blank">',
'</a>'
),
'type' => 'ppcp-heading',
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_cart-block_label' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
/**
* Returns default label ID of the PayPal button in block cart.
*/
'default' => apply_filters( 'woocommerce_paypal_payments_button_cart_block_label_default', 'paypal' ),
'desc_tip' => true,
'description' => __(
'This controls the label on the primary button.',
'woocommerce-paypal-payments'
),
'options' => array(
'paypal' => __( 'PayPal', 'woocommerce-paypal-payments' ),
'checkout' => __( 'Checkout', 'woocommerce-paypal-payments' ),
'buynow' => __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ),
'pay' => __( 'Pay with PayPal', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_cart-block_color' => array(
'title' => __( 'Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'gold',
'desc_tip' => true,
'description' => __(
'Controls the background color of the primary button. Use "Gold" to leverage PayPal\'s recognition and preference, or change it to match your site design or aesthetic.',
'woocommerce-paypal-payments'
),
'options' => array(
'gold' => __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ),
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_cart-block_shape' => array(
'title' => __( 'Shape', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'rect',
'desc_tip' => true,
'description' => __(
'The pill-shaped button\'s unique and powerful shape signifies PayPal in people\'s minds. Use the rectangular button as an alternative when pill-shaped buttons might pose design challenges.',
'woocommerce-paypal-payments'
),
'options' => array(
'pill' => __( 'Pill', 'woocommerce-paypal-payments' ),
'rect' => __( 'Rectangle', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_cart-block_height' => array(
'title' => __( 'Button Height', 'woocommerce-paypal-payments' ),
'type' => 'number',
'default' => '48',
'custom_attributes' => array(
'min' => 40,
'max' => 55,
),
'desc_tip' => true,
'description' => __( 'Set a value from 40 to 55.', 'woocommerce-paypal-payments' ),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'button_cart-block_preview' => array(
'type' => 'ppcp-text',
'text' => $render_preview_element( 'ppcpCartBlockButtonPreview' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
); );
return array_merge( $fields, $smart_button_fields ); return array_merge( $fields, $smart_button_fields );