Merge branch 'trunk' into PCP-3170-add-es-lint-and-testing-library-for-js-code

This commit is contained in:
Emili Castells Guasch 2024-07-01 17:29:03 +02:00
commit 4c05e7c2bd
34 changed files with 1094 additions and 450 deletions

View file

@ -20,6 +20,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
// Used in various places to mark fields for the preview button.
$apm_name = 'ApplePay';
// Eligibility check.
if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) {
return $fields;
@ -171,7 +174,7 @@ return array(
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
@ -183,10 +186,13 @@ return array(
->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' )
->action_visible( 'applepay_button_preview' )
->action_class( 'applepay_button_enabled', 'active' )
->to_array(),
)
),
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'is_enabled',
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
@ -253,56 +259,68 @@ return array(
'requirements' => array(),
),
'applepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'This controls the label of the Apple Pay button.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'type',
),
),
'applepay_button_color' => array(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'The Apple Pay Button may appear as a black button with white lettering, white button with black lettering, or a white button with black lettering and a black outline.',
'woocommerce-paypal-payments'
),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'color',
),
),
'applepay_button_language' => array(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'The language and region used for the displayed Apple Pay button. The default value is the current language and region setting in a browser.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'language',
),
),
'applepay_checkout_data_mode' => array(
'title' => __( 'Send checkout billing and shipping data to Apple Pay', 'woocommerce-paypal-payments' ),
@ -318,6 +336,22 @@ return array(
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_button_preview' => array(
'type' => 'ppcp-text',
'text' => sprintf(
'
<div class="ppcp-preview ppcp-button-preview" data-ppcp-apm-preview="%1$s">
<h4>' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '</h4>
<div id="ppcp%1$sButtonPreview" class="ppcp-button-preview-inner"></div>
</div>',
$apm_name
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
)
);
},

View file

@ -135,6 +135,10 @@ class ApplepayButton {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
const wrapper_id = '#' + wrapper;
if (wrapper_id === ppcpButtonWrapper) {
throw new Error(`[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${wrapper_id}"`);
}
const syncButtonVisibility = () => {
if (!this.isEligible) {
return;

View file

@ -1,148 +1,112 @@
import {loadCustomScript} from "@paypal/paypal-js";
import ApplepayButton from "./ApplepayButton";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import ApplepayButton from './ApplepayButton';
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
(function ({
buttonConfig,
jQuery
}) {
let applePayConfig;
let buttonQueue = [];
let activeButtons = {};
let bootstrapped = false;
// React to PayPal config changes.
jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => {
if (bootstrapped) {
createButton(ppcpConfig);
} else {
buttonQueue.push({
ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig))
});
}
});
// React to ApplePay config changes.
jQuery([
'#ppcp-applepay_button_enabled',
'#ppcp-applepay_button_type',
'#ppcp-applepay_button_color',
'#ppcp-applepay_button_language'
].join(',')).on('change', () => {
for (const [selector, ppcpConfig] of Object.entries(activeButtons)) {
createButton(ppcpConfig);
}
});
// Maybe we can find a more elegant reload method when transitioning from styling modes.
jQuery([
'#ppcp-smart_button_enable_styling_per_location'
].join(',')).on('change', () => {
setTimeout(() => {
for (const [selector, ppcpConfig] of Object.entries(activeButtons)) {
createButton(ppcpConfig);
}
}, 100);
});
const applyConfigOptions = function (buttonConfig) {
buttonConfig.button = buttonConfig.button || {};
buttonConfig.button.type = jQuery('#ppcp-applepay_button_type').val();
buttonConfig.button.color = jQuery('#ppcp-applepay_button_color').val();
buttonConfig.button.lang = jQuery('#ppcp-applepay_button_language').val();
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
*/
const buttonManager = () => {
if (!ApplePayPreviewButtonManager.instance) {
ApplePayPreviewButtonManager.instance = new ApplePayPreviewButtonManager();
}
const createButton = function (ppcpConfig) {
const selector = ppcpConfig.button.wrapper + 'ApplePay';
return ApplePayPreviewButtonManager.instance;
};
if (!jQuery('#ppcp-applepay_button_enabled').is(':checked')) {
jQuery(selector).remove();
return;
}
buttonConfig = JSON.parse(JSON.stringify(buttonConfig));
buttonConfig.button.wrapper = selector.replace('#', '');
applyConfigOptions(buttonConfig);
/**
* Manages all Apple Pay preview buttons on this page.
*/
class ApplePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'ApplePay',
buttonConfig: window.wc_ppcp_applepay_admin,
};
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-applepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);
} else {
jQuery(selector).replaceWith(wrapperElement);
}
const button = new ApplepayButton(
'preview',
null,
buttonConfig,
ppcpConfig,
);
button.init(applePayConfig);
activeButtons[selector] = ppcpConfig;
super(args);
}
const bootstrap = async function () {
if (!widgetBuilder.paypal) {
return;
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig(payPal) {
const apiMethod = payPal?.Applepay()?.config;
if (!apiMethod) {
this.error('configuration object cannot be retrieved from PayPal');
return {};
}
applePayConfig = await widgetBuilder.paypal.Applepay().config();
return await apiMethod();
}
// We need to set bootstrapped here otherwise applePayConfig may not be set.
bootstrapped = true;
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {ApplePayPreviewButton}
*/
createButtonInstance(wrapperId) {
return new ApplePayPreviewButton({
selector: wrapperId,
apiConfig: this.apiConfig,
});
}
}
let options;
while (options = buttonQueue.pop()) {
createButton(options.ppcpConfig);
/**
* A single Apple Pay preview button instance.
*/
class ApplePayPreviewButton extends PreviewButton {
constructor(args) {
super(args);
this.selector = `${args.selector}ApplePay`;
this.defaultAttributes = {
button: {
type: 'pay',
color: 'black',
lang: 'en',
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass('ppcp-button-applepay');
return element;
}
createButton(buttonConfig) {
const button = new ApplepayButton('preview', null, buttonConfig, this.ppcpConfig);
button.init(this.apiConfig);
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
*/
dynamicPreviewConfig(buttonConfig, ppcpConfig) {
// The Apple Pay button expects the "wrapper" to be an ID without `#` prefix!
buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace(/^#/, '');
// Merge the current form-values into the preview-button configuration.
if (ppcpConfig.button) {
buttonConfig.button.type = ppcpConfig.button.style.type;
buttonConfig.button.color = ppcpConfig.button.style.color;
buttonConfig.button.lang =
ppcpConfig.button.style?.lang || ppcpConfig.button.style.language;
}
}
}
if (!window.ApplePaySession) {
jQuery('body').addClass('ppcp-non-ios-device')
}
};
document.addEventListener(
'DOMContentLoaded',
() => {
if (typeof (buttonConfig) === 'undefined') {
console.error('PayPal button could not be configured.');
return;
}
let paypalLoaded = false;
let applePayLoaded = false;
const tryToBoot = () => {
if (!bootstrapped && paypalLoaded && applePayLoaded) {
bootstrap();
}
}
// Load ApplePay SDK
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
applePayLoaded = true;
tryToBoot();
});
// Wait for PayPal to be loaded externally
if (typeof widgetBuilder.paypal !== 'undefined') {
paypalLoaded = true;
tryToBoot();
}
jQuery(document).on('ppcp-paypal-loaded', () => {
paypalLoaded = true;
tryToBoot();
});
},
);
})({
buttonConfig: window.wc_ppcp_applepay_admin,
jQuery: window.jQuery
});
// Initialize the preview button manager.
buttonManager();

View file

@ -205,7 +205,7 @@ class ApplepayModule implements ModuleInterface {
add_action(
'admin_enqueue_scripts',
static function () use ( $c, $button ) {
if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-standard-payments-page' ) ) {
if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) ) {
return;
}

View file

@ -69,14 +69,13 @@ class DataToAppleButtonScripts {
);
}
/**
* Returns the appropriate admin data to send to ApplePay script
*
* @return array
* @throws NotFoundException When the setting is not found.
*/
public function apple_pay_script_data_for_admin(): array {
public function apple_pay_script_data_for_admin() : array {
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'];
$currency_code = get_woocommerce_currency();
@ -250,11 +249,13 @@ class DataToAppleButtonScripts {
$lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang );
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
$is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' );
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'is_admin' => true,
'is_enabled' => $is_enabled,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),

View file

@ -1,6 +1,7 @@
export function log(message, level = 'info') {
const wpDebug = window.wc_ppcp_axo?.wp_debug;
const endpoint = window.wc_ppcp_axo?.ajax?.frontend_logger?.endpoint;
if(!endpoint) {
if (!endpoint) {
return;
}
@ -15,12 +16,14 @@ export function log(message, level = 'info') {
}
})
}).then(() => {
switch (level) {
case 'error':
console.error(`[AXO] ${message}`);
break;
default:
console.log(`[AXO] ${message}`);
if (wpDebug) {
switch (level) {
case 'error':
console.error(`[AXO] ${message}`);
break;
default:
console.log(`[AXO] ${message}`);
}
}
});
}

View file

@ -169,7 +169,7 @@ class AxoManager {
'email' => 'render',
),
'insights' => array(
'enabled' => true,
'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' =>
substr(
@ -216,6 +216,7 @@ class AxoManager {
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
);
}

View file

@ -74,6 +74,19 @@ class AxoModule implements ModuleInterface {
return $methods;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
if ( ! $is_dcc_enabled ) {
return $methods;
}
if ( $this->is_excluded_endpoint() ) {
return $methods;
}
$methods[] = $gateway;
return $methods;
},
@ -325,10 +338,13 @@ class AxoModule implements ModuleInterface {
*/
private function should_render_fastlane( Settings $settings ): bool {
$is_axo_enabled = $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout()
&& $is_axo_enabled;
&& $is_axo_enabled
&& $is_dcc_enabled
&& ! $this->is_excluded_endpoint();
}
/**
@ -373,4 +389,14 @@ class AxoModule implements ModuleInterface {
);
}
}
/**
* Condition to evaluate if the current endpoint is excluded.
*
* @return bool
*/
private function is_excluded_endpoint(): bool {
// Exclude the Order Pay endpoint.
return is_wc_endpoint_url( 'order-pay' );
}
}

View file

@ -0,0 +1,154 @@
import merge from 'deepmerge';
/**
* Base class for APM button previews, used on the plugin's settings page.
*/
class PreviewButton {
/**
* @param {string} selector - CSS ID of the wrapper, including the `#`
* @param {object} apiConfig - PayPal configuration object; retrieved via a
* widgetBuilder API method
*/
constructor({
selector,
apiConfig,
}) {
this.apiConfig = apiConfig;
this.defaultAttributes = {};
this.buttonConfig = {};
this.ppcpConfig = {};
this.isDynamic = true;
// The selector is usually overwritten in constructor of derived class.
this.selector = selector;
this.wrapper = selector;
this.domWrapper = null;
}
/**
* Creates a new DOM node to contain the preview button.
*
* @return {jQuery} Always a single jQuery element with the new DOM node.
*/
createNewWrapper() {
const previewId = this.selector.replace('#', '');
const previewClass = 'ppcp-button-apm';
return jQuery(`<div id='${previewId}' class='${previewClass}'>`);
}
/**
* Toggle the "dynamic" nature of the preview.
* When the button is dynamic, it will reflect current form values. A static button always
* uses the settings that were provided via PHP.
*
* @return {this} Reference to self, for chaining.
*/
setDynamic(state) {
this.isDynamic = state;
return this;
}
/**
* Sets server-side configuration for the button.
*
* @return {this} Reference to self, for chaining.
*/
setButtonConfig(config) {
this.buttonConfig = merge(this.defaultAttributes, config);
this.buttonConfig.button.wrapper = this.selector;
return this;
}
/**
* Updates the button configuration with current details from the form.
*
* @return {this} Reference to self, for chaining.
*/
setPpcpConfig(config) {
this.ppcpConfig = merge({}, config);
return this;
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
*/
dynamicPreviewConfig(previewConfig, formConfig) {
// Implement in derived class.
}
/**
* Responsible for creating the actual payment button preview.
* Called by the `render()` method, after the wrapper DOM element is ready.
*/
createButton(previewConfig) {
throw new Error('The "createButton" method must be implemented by the derived class');
}
/**
* Refreshes the button in the DOM.
* Will always create a new button in the DOM.
*/
render() {
// The APM button is disabled and cannot be enabled on the current page: Do not render it.
if (!this.isDynamic && !this.buttonConfig.is_enabled) {
return;
}
if (!this.domWrapper) {
if (!this.wrapper) {
console.error('Skip render, button is not configured yet');
return;
}
this.domWrapper = this.createNewWrapper();
this.domWrapper.insertAfter(this.wrapper);
} else {
this.domWrapper.empty().show();
}
this.isVisible = true;
const previewButtonConfig = merge({}, this.buttonConfig);
const previewPpcpConfig = this.isDynamic ? merge({}, this.ppcpConfig) : {};
previewButtonConfig.button.wrapper = this.selector;
this.dynamicPreviewConfig(previewButtonConfig, previewPpcpConfig);
/*
* previewButtonConfig.button.wrapper must be different from this.ppcpConfig.button.wrapper!
* If both selectors point to the same element, an infinite loop is triggered.
*/
const buttonWrapper = previewButtonConfig.button.wrapper.replace(/^#/, '');
const ppcpWrapper = this.ppcpConfig.button.wrapper.replace(/^#/, '');
if (buttonWrapper === ppcpWrapper) {
throw new Error(`[APM Preview Button] Infinite loop detected. Provide different selectors for the button/ppcp wrapper elements! Selector: "#${buttonWrapper}"`);
}
this.createButton(previewButtonConfig);
/*
* Unfortunately, a hacky way that is required to guarantee that this preview button is
* actually visible after calling the `render()` method. On some sites, we've noticed that
* certain JS events (like `ppcp-hidden`) do not fire in the expected order. This causes
* problems with preview buttons not being displayed instantly.
*
* Using a timeout here will make the button visible again at the end of the current
* event queue.
*/
setTimeout(() => this.domWrapper.show());
}
remove() {
this.isVisible = false;
if (this.domWrapper) {
this.domWrapper.hide().empty();
}
}
}
export default PreviewButton;

View file

@ -0,0 +1,286 @@
import { loadCustomScript } from '@paypal/paypal-js';
import widgetBuilder from './WidgetBuilder';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
/**
* Manages all PreviewButton instances of a certain payment method on the page.
*/
class PreviewButtonManager {
/**
* Resolves the promise.
* Used by `this.boostrap()` to process enqueued initialization logic.
*/
#onInitResolver;
/**
* A deferred Promise that is resolved once the page is ready.
* Deferred init logic can be added by using `this.#onInit.then(...)`
*
* @param {Promise<void>|null}
*/
#onInit;
constructor({
methodName,
buttonConfig,
defaultAttributes,
}) {
// Define the payment method name in the derived class.
this.methodName = methodName;
this.buttonConfig = buttonConfig;
this.defaultAttributes = defaultAttributes;
this.isEnabled = true;
this.buttons = {};
this.apiConfig = null;
this.#onInit = new Promise(resolve => {
this.#onInitResolver = resolve;
});
this.bootstrap = this.bootstrap.bind(this);
this.renderPreview = this.renderPreview.bind(this);
/**
* The "configureAllButtons" method applies ppcpConfig to all buttons that were created
* by this PreviewButtonManager instance. We debounce this method, as it should invoke
* only once, even if called multiple times in a row.
*
* This is required, as the `ppcp_paypal_render_preview` event does not fire for all
* buttons, but only a single time, passing in a random button's wrapper-ID; however,
* that event should always refresh all preview buttons, not only that single button.
*/
this._configureAllButtons = debounce(this._configureAllButtons.bind(this), 100);
this.registerEventListeners();
}
/**
* Protected method that needs to be implemented by the derived class.
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig(payPal) {
throw new Error('The "fetchConfig" method must be implemented by the derived class');
}
/**
* Protected method that needs to be implemented by the derived class.
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {PreviewButton}
*/
createButtonInstance(wrapperId) {
throw new Error('The "createButtonInstance" method must be implemented by the derived class');
}
registerEventListeners() {
jQuery(document).one('DOMContentLoaded', this.bootstrap);
// General event that all APM buttons react to.
jQuery(document).on('ppcp_paypal_render_preview', this.renderPreview);
// Specific event to only (re)render the current APM button type.
jQuery(document).on(`ppcp_paypal_render_preview_${this.methodName}`, this.renderPreview);
}
/**
* Output an error message to the console, with a module-specific prefix.
*/
error(message, ...args) {
console.error(`${this.methodName} ${message}`, ...args);
}
/**
* Whether this is a dynamic preview of the APM button.
* A dynamic preview adjusts to the current form settings, while a static preview uses the
* style settings that were provided from server-side.
*/
isDynamic() {
return !!document.querySelector(`[data-ppcp-apm-name="${this.methodName}"]`);
}
/**
* Load dependencies and bootstrap the module.
* Returns a Promise that resolves once all dependencies were loaded and the module can be
* used without limitation.
*
* @return {Promise<void>}
*/
async bootstrap() {
const MAX_WAIT_TIME = 10000; // Fail, if PayPal SDK is unavailable after 10 seconds.
const RESOLVE_INTERVAL = 200;
if (!this.buttonConfig || !widgetBuilder) {
this.error('Button could not be configured.');
return;
}
// This is a localization object of "gateway-settings.js". If it's missing, the script was
// not loaded.
if (!window.PayPalCommerceGatewaySettings) {
this.error(
'PayPal settings are not fully loaded. Please clear the cache and reload the page.');
return;
}
// A helper function that clears the interval and resolves/rejects the promise.
const resolveOrReject = (resolve, reject, id, success = true) => {
clearInterval(id);
success ? resolve() : reject('Timeout while waiting for widgetBuilder.paypal');
};
// Wait for the PayPal SDK to be ready.
const paypalPromise = new Promise((resolve, reject) => {
let elapsedTime = 0;
const id = setInterval(() => {
if (widgetBuilder.paypal) {
resolveOrReject(resolve, reject, id);
} else if (elapsedTime >= MAX_WAIT_TIME) {
resolveOrReject(resolve, reject, id, false);
}
elapsedTime += RESOLVE_INTERVAL;
}, RESOLVE_INTERVAL);
});
// Load the custom SDK script.
const customScriptPromise = loadCustomScript({ url: this.buttonConfig.sdk_url });
// Wait for both promises to resolve before continuing.
await Promise
.all([customScriptPromise, paypalPromise])
.catch(err => {
console.log(`Failed to load ${this.methodName} dependencies:`, err);
});
/*
The fetchConfig method requires two objects to succeed:
(a) the SDK custom-script
(b) the `widgetBuilder.paypal` object
*/
this.apiConfig = await this.fetchConfig(widgetBuilder.paypal);
await this.#onInitResolver();
this.#onInit = null;
}
/**
* Event handler, fires on `ppcp_paypal_render_preview`
*
* @param ev - Ignored
* @param ppcpConfig - The button settings for the preview.
*/
renderPreview(ev, ppcpConfig) {
const id = ppcpConfig.button.wrapper;
if (!id) {
this.error('Button did not provide a wrapper ID', ppcpConfig);
return;
}
if (!this.buttons[id]) {
this._addButton(id, ppcpConfig);
} else {
// This is a debounced method, that fires after 100ms.
this._configureAllButtons(ppcpConfig);
}
}
/**
* Applies a new configuration to an existing preview button.
*/
_configureButton(id, ppcpConfig) {
this.buttons[id]
.setDynamic(this.isDynamic())
.setPpcpConfig(ppcpConfig)
.render();
}
/**
* Apples the provided configuration to all existing preview buttons.
*/
_configureAllButtons(ppcpConfig) {
Object.entries(this.buttons).forEach(([id, button]) => {
this._configureButton(id, {
...ppcpConfig,
button: {
...ppcpConfig.button,
// The ppcpConfig object might refer to a different wrapper.
// Fix the selector, to avoid unintentionally hidden preview buttons.
wrapper: button.wrapper,
},
});
});
}
/**
* Creates a new preview button, that is rendered once the bootstrapping Promise resolves.
*/
_addButton(id, ppcpConfig) {
const createButton = () => {
if (!this.buttons[id]) {
this.buttons[id] = this.createButtonInstance(id).setButtonConfig(this.buttonConfig);
}
this._configureButton(id, ppcpConfig);
};
if (this.#onInit) {
this.#onInit.then(createButton);
} else {
createButton();
}
}
/**
* Refreshes all buttons using the latest buttonConfig.
*
* @return {this} Reference to self, for chaining.
*/
renderButtons() {
if (this.isEnabled) {
Object.values(this.buttons).forEach(button => button.render());
} else {
Object.values(this.buttons).forEach(button => button.remove());
}
return this;
}
/**
* Enables this payment method, which re-creates or refreshes all buttons.
*
* @return {this} Reference to self, for chaining.
*/
enable() {
if (!this.isEnabled) {
this.isEnabled = true;
this.renderButtons();
}
return this;
}
/**
* Disables this payment method, effectively removing all preview buttons.
*
* @return {this} Reference to self, for chaining.
*/
disable() {
if (!this.isEnabled) {
this.isEnabled = false;
this.renderButtons();
}
return this;
}
}
export default PreviewButtonManager;

View file

@ -707,7 +707,11 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
&& $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' )
&& $this->dcc_applies->for_country_currency()
&& in_array( $this->context(), array( 'checkout', 'pay-now', 'add-payment-method' ), true );
&& in_array(
$this->context(),
apply_filters( 'woocommerce_paypal_payments_can_render_dcc_contexts', array( 'checkout', 'pay-now', 'add-payment-method' ) ),
true
);
}
/**

View file

@ -20,6 +20,9 @@ return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
// Used in various places to mark fields for the preview button.
$apm_name = 'GooglePay';
// Eligibility check.
if ( ! $container->has( 'googlepay.eligible' ) || ! $container->get( 'googlepay.eligible' ) ) {
return $fields;
@ -133,7 +136,7 @@ return array(
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
@ -142,64 +145,79 @@ return array(
->action_visible( 'googlepay_button_color' )
->action_visible( 'googlepay_button_language' )
->action_visible( 'googlepay_button_shipping_enabled' )
->action_visible( 'googlepay_button_preview' )
->action_class( 'googlepay_button_enabled', 'active' )
->to_array(),
)
),
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'is_enabled',
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
'googlepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'This controls the label of the Google Pay button.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'type',
),
),
'googlepay_button_color' => array(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'Google Pay payment buttons exist in two styles: dark and light. To provide contrast, use dark buttons on light backgrounds and light buttons on dark or colorful backgrounds.',
'woocommerce-paypal-payments'
),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'color',
),
),
'googlepay_button_language' => array(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'The language and region used for the displayed Google Pay button. The default value is the current language and region setting in a browser.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'language',
),
),
'googlepay_button_shipping_enabled' => array(
'title' => __( 'Shipping Callback', 'woocommerce-paypal-payments' ),
@ -209,13 +227,29 @@ return array(
'Synchronizes your available shipping options with Google Pay. Enabling this may impact the buyer experience.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'classes' => array( 'ppcp-field-indent ppcp' ),
'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ),
'default' => 'no',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => array(),
),
'googlepay_button_preview' => array(
'type' => 'ppcp-text',
'text' => sprintf(
'
<div class="ppcp-preview ppcp-button-preview" data-ppcp-apm-preview="%1$s">
<h4>' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '</h4>
<div id="ppcp%1$sButtonPreview" class="ppcp-button-preview-inner"></div>
</div>',
$apm_name
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
)
);
},

View file

@ -139,6 +139,10 @@ class GooglepayButton {
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
if (wrapper === ppcpButtonWrapper) {
throw new Error(`[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${wrapper}"`);
}
const syncButtonVisibility = () => {
const $ppcpButtonWrapper = jQuery(ppcpButtonWrapper);
setVisible(wrapper, $ppcpButtonWrapper.is(':visible'));

View file

@ -1,146 +1,108 @@
import {loadCustomScript} from "@paypal/paypal-js";
import GooglepayButton from "./GooglepayButton";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import GooglepayButton from './GooglepayButton';
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
(function ({
buttonConfig,
jQuery
}) {
let googlePayConfig;
let buttonQueue = [];
let activeButtons = {};
let bootstrapped = false;
// React to PayPal config changes.
jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => {
if (bootstrapped) {
createButton(ppcpConfig);
} else {
buttonQueue.push({
ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig))
});
}
});
// React to GooglePay config changes.
jQuery([
'#ppcp-googlepay_button_enabled',
'#ppcp-googlepay_button_type',
'#ppcp-googlepay_button_color',
'#ppcp-googlepay_button_language',
'#ppcp-googlepay_button_shipping_enabled'
].join(',')).on('change', () => {
for (const [selector, ppcpConfig] of Object.entries(activeButtons)) {
createButton(ppcpConfig);
}
});
// Maybe we can find a more elegant reload method when transitioning from styling modes.
jQuery([
'#ppcp-smart_button_enable_styling_per_location'
].join(',')).on('change', () => {
setTimeout(() => {
for (const [selector, ppcpConfig] of Object.entries(activeButtons)) {
createButton(ppcpConfig);
}
}, 100);
});
const applyConfigOptions = function (buttonConfig) {
buttonConfig.button = buttonConfig.button || {};
buttonConfig.button.style = buttonConfig.button.style || {};
buttonConfig.button.style.type = jQuery('#ppcp-googlepay_button_type').val();
buttonConfig.button.style.color = jQuery('#ppcp-googlepay_button_color').val();
buttonConfig.button.style.language = jQuery('#ppcp-googlepay_button_language').val();
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
*/
const buttonManager = () => {
if (!GooglePayPreviewButtonManager.instance) {
GooglePayPreviewButtonManager.instance = new GooglePayPreviewButtonManager();
}
const createButton = function (ppcpConfig) {
const selector = ppcpConfig.button.wrapper + 'GooglePay';
return GooglePayPreviewButtonManager.instance;
};
if (!jQuery('#ppcp-googlepay_button_enabled').is(':checked')) {
jQuery(selector).remove();
return;
}
buttonConfig = JSON.parse(JSON.stringify(buttonConfig));
buttonConfig.button.wrapper = selector;
applyConfigOptions(buttonConfig);
/**
* Manages all GooglePay preview buttons on this page.
*/
class GooglePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'GooglePay',
buttonConfig: window.wc_ppcp_googlepay_admin,
};
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-googlepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);
} else {
jQuery(selector).replaceWith(wrapperElement);
}
const button = new GooglepayButton(
'preview',
null,
buttonConfig,
ppcpConfig,
);
button.init(googlePayConfig);
activeButtons[selector] = ppcpConfig;
super(args);
}
const bootstrap = async function () {
if (!widgetBuilder.paypal) {
return;
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig(payPal) {
const apiMethod = payPal?.Googlepay()?.config;
if (!apiMethod) {
this.error('configuration object cannot be retrieved from PayPal');
return {};
}
googlePayConfig = await widgetBuilder.paypal.Googlepay().config();
return await apiMethod();
}
// We need to set bootstrapped here otherwise googlePayConfig may not be set.
bootstrapped = true;
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {GooglePayPreviewButton}
*/
createButtonInstance(wrapperId) {
return new GooglePayPreviewButton({
selector: wrapperId,
apiConfig: this.apiConfig,
});
}
}
let options;
while (options = buttonQueue.pop()) {
createButton(options.ppcpConfig);
/**
* A single GooglePay preview button instance.
*/
class GooglePayPreviewButton extends PreviewButton {
constructor(args) {
super(args);
this.selector = `${args.selector}GooglePay`;
this.defaultAttributes = {
button: {
style: {
type: 'pay',
color: 'black',
language: 'en',
},
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass('ppcp-button-googlepay');
return element;
}
createButton(buttonConfig) {
const button = new GooglepayButton('preview', null, buttonConfig, this.ppcpConfig);
button.init(this.apiConfig);
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
*/
dynamicPreviewConfig(buttonConfig, ppcpConfig) {
// Merge the current form-values into the preview-button configuration.
if (ppcpConfig.button && buttonConfig.button) {
Object.assign(buttonConfig.button.style, ppcpConfig.button.style);
}
};
}
}
document.addEventListener(
'DOMContentLoaded',
() => {
if (typeof (buttonConfig) === 'undefined') {
console.error('PayPal button could not be configured.');
return;
}
let paypalLoaded = false;
let googlePayLoaded = false;
const tryToBoot = () => {
if (!bootstrapped && paypalLoaded && googlePayLoaded) {
bootstrap();
}
}
// Load GooglePay SDK
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
googlePayLoaded = true;
tryToBoot();
});
// Wait for PayPal to be loaded externally
if (typeof widgetBuilder.paypal !== 'undefined') {
paypalLoaded = true;
tryToBoot();
}
jQuery(document).on('ppcp-paypal-loaded', () => {
paypalLoaded = true;
tryToBoot();
});
},
);
})({
buttonConfig: window.wc_ppcp_googlepay_admin,
jQuery: window.jQuery
});
// Initialize the preview button manager.
buttonManager();

View file

@ -418,9 +418,12 @@ class Button implements ButtonInterface {
$shipping['countries'] = array_keys( $this->wc_countries()->get_shipping_countries() );
}
$is_enabled = $this->settings->has( 'googlepay_button_enabled' ) && $this->settings->get( 'googlepay_button_enabled' );
return array(
'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION',
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'is_enabled' => $is_enabled,
'sdk_url' => $this->sdk_url,
'button' => array(
'wrapper' => '#ppc-button-googlepay-container',

View file

@ -112,7 +112,7 @@ class GooglepayModule implements ModuleInterface {
add_action(
'admin_enqueue_scripts',
static function () use ( $c, $button ) {
if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-standard-payments-page' ) ) {
if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) ) {
return;
}

View file

@ -13,10 +13,6 @@ use Exception;
use WC_Order;
use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentInterface;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WP_Post;
use function WooCommerce\PayPalCommerce\Api\ppcp_get_paypal_order;
/**
* Class MetaBoxRenderer
@ -29,8 +25,6 @@ use function WooCommerce\PayPalCommerce\Api\ppcp_get_paypal_order;
*/
class MetaBoxRenderer {
use TransactionIdHandlingTrait;
/**
* Allowed shipping statuses.
*
@ -85,21 +79,10 @@ class MetaBoxRenderer {
/**
* Renders the order tracking MetaBox.
*
* @param mixed $post_or_order_object Either WP_Post or WC_Order when COT is data source.
* @param WC_Order $wc_order The WC order.
* @param string $capture_id The PayPal order capture ID.
*/
public function render( $post_or_order_object ): void {
$wc_order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
if ( ! $wc_order instanceof WC_Order ) {
return;
}
$paypal_order = ppcp_get_paypal_order( $wc_order );
$capture_id = $this->get_paypal_order_transaction_id( $paypal_order ) ?? '';
if ( ! $capture_id ) {
return;
}
public function render( WC_Order $wc_order, string $capture_id ): void {
$order_items = $wc_order->get_items();
$order_item_count = ! empty( $order_items ) ? count( $order_items ) : 0;

View file

@ -10,6 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Exception;
use WC_Order;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -18,13 +20,16 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\OrderTracking\Assets\OrderEditPageAssets;
use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WP_Post;
use function WooCommerce\PayPalCommerce\Api\ppcp_get_paypal_order;
/**
* Class OrderTrackingModule
*/
class OrderTrackingModule implements ModuleInterface {
use TrackingAvailabilityTrait;
use TrackingAvailabilityTrait, TransactionIdHandlingTrait;
public const PPCP_TRACKING_INFO_META_NAME = '_ppcp_paypal_tracking_info_meta_name';
@ -80,13 +85,41 @@ class OrderTrackingModule implements ModuleInterface {
);
$meta_box_renderer = $c->get( 'order-tracking.meta-box.renderer' );
assert( $meta_box_renderer instanceof MetaBoxRenderer );
add_action(
'add_meta_boxes',
function() use ( $meta_box_renderer, $bearer ) {
/**
* Adds the tracking metabox.
*
* @param string $post_type The post type.
* @param WP_Post|WC_Order $post_or_order_object The post/order object.
* @return void
*
* @psalm-suppress MissingClosureParamType
*/
function( string $post_type, $post_or_order_object ) use ( $meta_box_renderer, $bearer ) {
if ( ! $this->is_tracking_enabled( $bearer ) ) {
return;
}
$wc_order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
if ( ! $wc_order instanceof WC_Order ) {
return;
}
try {
$paypal_order = ppcp_get_paypal_order( $wc_order );
} catch ( Exception $exception ) {
return;
}
$capture_id = $this->get_paypal_order_transaction_id( $paypal_order ) ?? '';
if ( ! $capture_id ) {
return;
}
/**
* Class and function exist in WooCommerce.
*
@ -100,7 +133,9 @@ class OrderTrackingModule implements ModuleInterface {
add_meta_box(
'ppcp_order-tracking',
__( 'PayPal Package Tracking', 'woocommerce-paypal-payments' ),
array( $meta_box_renderer, 'render' ),
static function () use ( $meta_box_renderer, $wc_order, $capture_id ): void {
$meta_box_renderer->render( $wc_order, $capture_id );
},
$screen,
'side',
'high'

View file

@ -94,13 +94,27 @@ class PayPalSubscriptionsModule implements ModuleInterface {
*
* @psalm-suppress MissingClosureParamType
*/
static function ( $passed_validation, $product_id ) {
static function ( $passed_validation, $product_id ) use ( $c ) {
if ( WC()->cart->is_empty() ) {
return $passed_validation;
}
$product = wc_get_product( $product_id );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( 'subscriptions_api' !== $subscriptions_mode ) {
if ( $product && $product->get_sold_individually() ) {
$product->set_sold_individually( false );
$product->save();
}
return $passed_validation;
}
if ( $product && $product->get_meta( '_ppcp_enable_subscription_product', true ) === 'yes' ) {
if ( ! $product->get_sold_individually() ) {
$product->set_sold_individually( true );

View file

@ -84,6 +84,10 @@ class SavePaymentMethodsModule implements ModuleInterface {
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function( array $localized_script_data ) use ( $c ) {
if ( ! is_user_logged_in() ) {
return $localized_script_data;
}
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="50px" viewBox="0 0 80 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Giropay_acceptancemark_80x50</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Giropay_acceptancemark_80x50">
<rect id="Rectangle" fill="#FFFFFF" fill-rule="nonzero" x="0.38" y="0.38" width="79.25" height="49.25"></rect>
<path d="M79.25,0.75 L79.25,49.25 L0.75,49.25 L0.75,0.75 L79.25,0.75 M80,0 L0,0 L0,50 L80,50 L80,0 Z" id="Shape" fill="#BFBFBF" fill-rule="nonzero"></path>
<g id="Giropay" transform="translate(10.000000, 12.000000)">
<path d="M0.0485052988,4.66119138 C0.0485052988,2.09222708 2.12902174,0.00933576963 4.69412828,0.00933576963 L55.3234715,0.00933576963 C57.8896803,0.00933576963 59.9690706,2.09222708 59.9690706,4.66119138 L59.9690706,21.2852526 C59.9690706,23.8527219 57.8896803,25.9359985 55.3234715,25.9359985 L4.69412828,25.9359985 C2.12902174,25.9359985 0.0485052988,23.8527219 0.0485052988,21.2852526 L0.0485052988,4.66119138 L0.0485052988,4.66119138 Z" id="_92653320" fill="#000268"></path>
<path d="M1.95517995,4.82036195 L1.95517995,21.1276014 C1.95517995,22.7326433 3.25562557,24.034417 4.85974897,24.034417 L31.3849373,24.034417 L31.3849373,1.91354603 L4.85974897,1.91354603 C3.25562557,1.91354603 1.95517995,3.21529585 1.95517995,4.82036195 L1.95517995,4.82036195 Z M38.2385635,12.8925274 C38.2385635,13.9307775 37.7282077,14.6443441 36.8832548,14.6443441 C36.1367871,14.6443441 35.5145236,13.9307775 35.5145236,12.9799746 C35.5145236,12.0040421 36.0617905,11.2784658 36.8832548,11.2784658 C37.754333,11.2784658 38.2385635,12.0291961 38.2385635,12.8925274 Z M33.2773508,18.8472659 L35.5145236,18.8472659 L35.5145236,15.3064695 L35.5395227,15.3064695 C35.9633053,16.0823531 36.8093844,16.3698729 37.5924277,16.3698729 C39.5192354,16.3698729 40.5510927,14.7689549 40.5510927,12.8422195 C40.5510927,11.2664554 39.5692336,9.55141717 37.7782052,9.55141717 C36.7594106,9.55141717 35.8144627,9.96470555 35.3656809,10.8783204 L35.3406818,10.8783204 L35.3406818,9.7027011 L33.2773508,9.7027011 L33.2773508,18.8472659 Z M43.6067337,14.1556176 C43.6067337,13.5306321 44.2028719,13.2937816 44.9624023,13.2937816 C45.2977901,13.2937816 45.6216014,13.3174403 45.9073508,13.330946 C45.9073508,14.0936859 45.372763,14.8695463 44.5266832,14.8695463 C44.0044153,14.8695463 43.6067337,14.6071798 43.6067337,14.1556176 Z M48.1195488,16.2201082 C48.0206794,15.7077475 47.9956809,15.1938684 47.9956809,14.6815078 L47.9956809,12.2543983 C47.9956809,10.265369 46.5665256,9.55141717 44.9124284,9.55141717 C43.9555441,9.55141717 43.1225269,9.68917157 42.3264216,10.014989 L42.3644834,11.5419649 C42.9841103,11.1909942 43.7067051,11.0532391 44.4278144,11.0532391 C45.2347292,11.0532391 45.8939283,11.2916094 45.9073508,12.1800948 C45.6216014,12.1297868 45.2227935,12.0914892 44.8624309,12.0914892 C43.6697939,12.0914892 41.5184036,12.3298595 41.5184036,14.3185267 C41.5184036,15.7329015 42.6618095,16.3698729 43.9424814,16.3698729 C44.8624309,16.3698729 45.4846708,16.008411 45.9942595,15.1938684 L46.0192586,15.1938684 C46.0192586,15.5328288 46.0558099,15.8691368 46.0692561,16.2201082 L48.1195488,16.2201082 Z M49.1263833,18.8472659 C49.5867172,18.9478812 50.0463328,18.9985505 50.5186032,18.9985505 C52.5704056,18.9985505 53.0546124,17.409257 53.6888124,15.7700414 L56.0644018,9.7027011 L53.8257193,9.7027011 L52.495409,13.9559315 L52.4704099,13.9559315 L51.0778065,9.7027011 L48.6667921,9.7027011 L51.28971,16.3698729 C51.1278041,16.9456845 50.7055076,17.2715019 50.1578807,17.2715019 C49.8460059,17.2715019 49.5736789,17.2332043 49.2748667,17.1337468 L49.1263833,18.8472659 Z" id="_92186184" fill="#FFFFFF"></path>
<path d="M7.42075671,12.905671 C7.42075671,12.0171857 7.85647576,11.2784658 8.6894929,11.2784658 C9.69637475,11.2784658 10.1186719,12.0914892 10.1186719,12.8170662 C10.1186719,13.8181764 9.48447193,14.4934454 8.6894929,14.4934454 C8.01838174,14.4934454 7.42075671,13.9187672 7.42075671,12.905671 Z M12.2823578,9.7027011 L10.2555781,9.7027011 L10.2555781,10.8783204 L10.2320652,10.8783204 C9.75828501,10.0773066 8.98681894,9.55141717 8.03031747,9.55141717 C6.01657678,9.55141717 5.10858737,11.0040651 5.10858737,12.9428109 C5.10858737,14.8695463 6.21541759,16.2201082 7.99189651,16.2201082 C8.88833303,16.2201082 9.63444081,15.8691368 10.1686458,15.1064206 L10.1936442,15.1064206 L10.1936442,15.4573675 C10.1936442,16.732854 9.49753461,17.3458299 8.20528613,17.3458299 C7.27227396,17.3458299 6.69964803,17.1457572 6.01657678,16.808316 L5.904669,18.5852621 C6.42583495,18.7733245 7.30884893,18.9985505 8.37912785,18.9985505 C10.9901094,18.9985505 12.2823578,18.1348332 12.2823578,15.4573675 L12.2823578,9.7027011 Z M16.0647166,6.98807173 L13.8264169,6.98807173 L13.8264169,8.6392977 L16.0647166,8.6392977 L16.0647166,6.98807173 Z M13.8275432,16.2201082 L16.0647166,16.2201082 L16.0647166,9.7027011 L13.8275432,9.7027011 L13.8275432,16.2201082 Z M22.2961752,9.62687843 C22.0723353,9.59007618 21.7985222,9.55141717 21.538131,9.55141717 C20.5682077,9.55141717 20.0090043,10.0773066 19.6232591,10.9034743 L19.59826,10.9034743 L19.59826,9.7027011 L17.5595445,9.7027011 L17.5595445,16.2201082 L19.7971008,16.2201082 L19.7971008,13.4687004 C19.7971008,12.1921045 20.3824295,11.4293638 21.4262232,11.4293638 C21.6881006,11.4293638 21.9354284,11.4293638 22.1842674,11.5033059 L22.2961752,9.62687843 Z M26.1494557,14.7941089 C25.1175977,14.7941089 24.6953012,13.9307775 24.6953012,12.9679642 C24.6953012,11.9920317 25.1175977,11.1287004 26.1494557,11.1287004 C27.1824392,11.1287004 27.6050956,11.9920317 27.6050956,12.9679642 C27.6050956,13.9307775 27.1824392,14.7941089 26.1494557,14.7941089 Z M26.1494557,16.3698729 C28.2881425,16.3698729 29.9172649,15.1199263 29.9172649,12.9679642 C29.9172649,10.8028836 28.2881425,9.55141717 26.1494557,9.55141717 C24.0111274,9.55141717 22.3831319,10.8028836 22.3831319,12.9679642 C22.3831319,15.1199263 24.0111274,16.3698729 26.1494557,16.3698729 Z" id="_47303032" fill="#FF0007"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -2,10 +2,24 @@
width: 350px;
padding: 15px;
border: 1px solid lightgray;
background: #eeeeef;
border-radius: 15px;
box-shadow: 0 2px 10px 1px #ddd;
margin-right: -28px;
// Preview box showing a single button.
&[data-ppcp-apm-preview] {
height: 82px;
@media (min-width: 1200px) {
margin-top: -149px;
}
@media (min-width: 601px) and (max-width: 1399px) {
margin-right: 10px;
}
}
h4 {
margin-top: 0;
}

View file

@ -1,5 +1,6 @@
import { loadScript } from "@paypal/paypal-js";
import {debounce} from "./helper/debounce";
import { buttonRefreshTriggerFactory, buttonSettingsGetterFactory } from './helper/preview-button';
import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer'
import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer";
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
@ -312,6 +313,7 @@ document.addEventListener(
const payLaterMessagingLocations = ['product', 'cart', 'checkout', 'shop', 'home', 'general'];
const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express', 'general'];
// Default preview buttons; on "Standard Payments" tab.
paypalButtonLocations.forEach((location) => {
const inputNamePrefix = location === 'checkout' ? '#ppcp-button' : '#ppcp-button_' + location;
const wrapperName = location.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
@ -330,6 +332,31 @@ document.addEventListener(
createButtonPreview(() => getButtonSettings('#ppcp' + wrapperName + 'ButtonPreview', fields));
});
/**
* Inspect DOM to find APM button previews; on tabs like "Advanced Card Payments".
*
* How it works:
*
* 1. Add a <div> to hold the preview button to the settings page:
* - `id="ppcp[NAME]ButtonPreview"`
* - `data-ppc-apm-preview="[NAME]"`
* 2. Mark all fields that are relevant for the preview button:
* - custom_attribute: `data-ppcp-apm-name="[NAME]"`
* - custom_attribute: `data-ppcp-field-name="[FIELD]"`
*
* This block will find all marked input fields and trigger a re-render of the
* preview button when one of those fields value changes.
*
* Example: See the ppcp-google-pay "extensions.php" file.
*/
document.querySelectorAll('[data-ppcp-apm-preview]').forEach(item => {
const apmName = item.dataset.ppcpApmPreview;
const getSettings = buttonSettingsGetterFactory(apmName)
const renderButtonPreview = buttonRefreshTriggerFactory(apmName);
renderPreview(getSettings, renderButtonPreview)
});
payLaterMessagingLocations.forEach((location) => {
const inputNamePrefix = '#ppcp-pay_later_' + location + '_message';
const wrapperName = location.charAt(0).toUpperCase() + location.slice(1);

View file

@ -0,0 +1,68 @@
/**
* Returns a Map with all input fields that are relevant to render the preview of the
* given payment button.
*
* @param {string} apmName - Value of the custom attribute `data-ppcp-apm-name`.
* @return {Map<string, {val:Function, el:HTMLInputElement}>}
*/
export function getButtonFormFields(apmName) {
const inputFields = document.querySelectorAll(`[data-ppcp-apm-name="${apmName}"]`);
return [...inputFields].reduce((fieldMap, el) => {
const key = el.dataset.ppcpFieldName;
let getter = () => el.value;
if ('LABEL' === el.tagName) {
el = el.querySelector('input[type="checkbox"]');
getter = () => el.checked;
}
return fieldMap.set(key, {
val: getter,
el,
});
}, new Map());
}
/**
* Returns a function that triggers an update of the specified preview button, when invoked.
* @param {string} apmName
* @return {((object) => void)}
*/
export function buttonRefreshTriggerFactory(apmName) {
const eventName = `ppcp_paypal_render_preview_${apmName}`;
return (settings) => {
jQuery(document).trigger(eventName, settings);
};
}
/**
* Returns a function that gets the current form values of the specified preview button.
*
* @param {string} apmName
* @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}}
*/
export function buttonSettingsGetterFactory(apmName) {
const fields = getButtonFormFields(apmName);
return () => {
const buttonConfig = {
wrapper: `#ppcp${apmName}ButtonPreview`,
'is_enabled': true,
style: {},
};
fields.forEach((item, name) => {
if ('is_enabled' === name) {
buttonConfig[name] = item.val();
} else {
buttonConfig.style[name] = item.val();
}
});
return { button: buttonConfig };
};
}

View file

@ -201,9 +201,24 @@ return array(
);
},
'wcgateway.is-ppcp-settings-standard-payments-page' => static function ( ContainerInterface $container ): bool {
return $container->get( 'wcgateway.is-ppcp-settings-page' )
&& $container->get( 'wcgateway.current-ppcp-settings-page-id' ) === PayPalGateway::ID;
// Checks, if the current admin page contains settings for this plugin's payment methods.
'wcgateway.is-ppcp-settings-payment-methods-page' => static function ( ContainerInterface $container ) : bool {
if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) {
return false;
}
$active_tab = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
return in_array(
$active_tab,
array(
PayPalGateway::ID,
CreditCardGateway::ID,
CardButtonGateway::ID,
Settings::CONNECTION_TAB_ID,
),
true
);
},
'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string {
@ -1025,7 +1040,6 @@ return array(
'bancontact' => _x( 'Bancontact', 'Name of payment method', 'woocommerce-paypal-payments' ),
'blik' => _x( 'BLIK', 'Name of payment method', 'woocommerce-paypal-payments' ),
'eps' => _x( 'eps', 'Name of payment method', 'woocommerce-paypal-payments' ),
'giropay' => _x( 'giropay', 'Name of payment method', 'woocommerce-paypal-payments' ),
'ideal' => _x( 'iDEAL', 'Name of payment method', 'woocommerce-paypal-payments' ),
'mybank' => _x( 'MyBank', 'Name of payment method', 'woocommerce-paypal-payments' ),
'p24' => _x( 'Przelewy24', 'Name of payment method', 'woocommerce-paypal-payments' ),

View file

@ -112,6 +112,13 @@ class SettingsPageAssets {
*/
private $billing_agreements_endpoint;
/**
* Whether we're on a settings page for our plugin's payment methods.
*
* @var bool
*/
private $is_paypal_payment_method_page;
/**
* Assets constructor.
*
@ -128,6 +135,7 @@ class SettingsPageAssets {
* @param bool $is_settings_page Whether it's a settings page of this plugin.
* @param bool $is_acdc_enabled Whether the ACDC gateway is enabled.
* @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint.
* @param bool $is_paypal_payment_method_page Whether we're on a settings page for our plugin's payment methods.
*/
public function __construct(
string $module_url,
@ -142,21 +150,23 @@ class SettingsPageAssets {
array $all_funding_sources,
bool $is_settings_page,
bool $is_acdc_enabled,
BillingAgreementsEndpoint $billing_agreements_endpoint
BillingAgreementsEndpoint $billing_agreements_endpoint,
bool $is_paypal_payment_method_page
) {
$this->module_url = $module_url;
$this->version = $version;
$this->subscription_helper = $subscription_helper;
$this->client_id = $client_id;
$this->currency = $currency;
$this->country = $country;
$this->environment = $environment;
$this->is_pay_later_button_enabled = $is_pay_later_button_enabled;
$this->disabled_sources = $disabled_sources;
$this->all_funding_sources = $all_funding_sources;
$this->is_settings_page = $is_settings_page;
$this->is_acdc_enabled = $is_acdc_enabled;
$this->billing_agreements_endpoint = $billing_agreements_endpoint;
$this->module_url = $module_url;
$this->version = $version;
$this->subscription_helper = $subscription_helper;
$this->client_id = $client_id;
$this->currency = $currency;
$this->country = $country;
$this->environment = $environment;
$this->is_pay_later_button_enabled = $is_pay_later_button_enabled;
$this->disabled_sources = $disabled_sources;
$this->all_funding_sources = $all_funding_sources;
$this->is_settings_page = $is_settings_page;
$this->is_acdc_enabled = $is_acdc_enabled;
$this->billing_agreements_endpoint = $billing_agreements_endpoint;
$this->is_paypal_payment_method_page = $is_paypal_payment_method_page;
}
/**
@ -176,7 +186,7 @@ class SettingsPageAssets {
$this->register_admin_assets();
}
if ( $this->is_paypal_payment_method_page() ) {
if ( $this->is_paypal_payment_method_page ) {
$this->register_paypal_admin_assets();
}
}
@ -184,30 +194,6 @@ class SettingsPageAssets {
}
/**
* Check whether the current page is PayPal payment method settings.
*
* @return bool
*/
private function is_paypal_payment_method_page(): bool {
if ( ! function_exists( 'get_current_screen' ) ) {
return false;
}
$screen = get_current_screen();
if ( ! $screen || $screen->id !== 'woocommerce_page_wc-settings' ) {
return false;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) );
$section = wc_clean( wp_unslash( $_GET['section'] ?? '' ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return 'checkout' === $tab && in_array( $section, array( PayPalGateway::ID, CardButtonGateway::ID ), true );
}
/**
* Register assets for PayPal admin pages.
*/

View file

@ -404,8 +404,10 @@ class AuthorizedPaymentsProcessor {
private function all_authorizations( Order $order ): array {
$authorizations = array();
foreach ( $order->purchase_units() as $purchase_unit ) {
foreach ( $purchase_unit->payments()->authorizations() as $authorization ) {
$authorizations[] = $authorization;
if ( ! is_null( $purchase_unit->payments() ) ) {
foreach ( $purchase_unit->payments()->authorizations() as $authorization ) {
$authorizations[] = $authorization;
}
}
}

View file

@ -154,8 +154,10 @@ class Settings implements ContainerInterface {
'woocommerce-paypal-payments'
),
);
foreach ( $defaults as $key => $value ) {
if ( isset( $this->settings[ $key ] ) ) {
$this->settings[ $key ] = apply_filters( 'woocommerce_paypal_payments_settings_value', $this->settings[ $key ], $key );
continue;
}
$this->settings[ $key ] = $value;

View file

@ -183,7 +183,8 @@ class WCGatewayModule implements ModuleInterface {
$c->get( 'wcgateway.settings.funding-sources' ),
$c->get( 'wcgateway.is-ppcp-settings-page' ),
$settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ),
$c->get( 'api.endpoint.billing-agreements' )
$c->get( 'api.endpoint.billing-agreements' ),
$c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' )
);
$assets->register_assets();
}