Merge pull request #1717 from woocommerce/feat/PCP-154-apple-pay-payment

Feat/pcp 154 apple pay payment
This commit is contained in:
Emili Castells 2023-09-21 15:16:17 +02:00 committed by GitHub
commit af7a02f387
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 978 additions and 169 deletions

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Applepay;
use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
return array(
@ -23,12 +24,55 @@ return array(
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
};
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) {
$connection_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading' );
$connection_link = '<a href="' . $connection_url . '" target="_blank">';
return $insert_after(
$fields,
'allow_card_button_gateway',
array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ),
'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">'
. sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Your PayPal account %1$srequires additional permissions%2$s to enable Apple Pay.', 'woocommerce-paypal-payments' ),
$connection_link,
'</a>'
)
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
->condition_element( 'applepay_button_enabled', '1' )
->action_enable( 'applepay_button_enabled' )
->to_array(),
)
),
),
),
)
);
}
return $insert_after(
$fields,
'allow_card_button_gateway',
array(
'applepay_button_enabled' => array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' )
@ -45,40 +89,20 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-handlers' => wp_json_encode(
'data-ppcp-display' => wp_json_encode(
array(
array(
'handler' => 'SubElementsHandler',
'options' => array(
'values' => array( '1' ),
'elements' => array( '#field-applepay_sandbox_validation_file', '#field-applepay_live_validation_file', '#field-applepay_button_color', '#field-applepay_button_type', '#field-applepay_button_language' ),
),
),
$display_manager
->rule()
->condition_element( 'applepay_button_enabled', '1' )
->action_visible( 'applepay_button_color' )
->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' )
->to_array(),
)
),
),
),
'applepay_live_validation_file' => array(
'title' => __( 'Apple Pay Live Validation File', 'woocommerce-paypal-payments' ),
'type' => 'text',
'desc_tip' => true,
'description' => __( 'Paste here the validation file content', 'woocommerce-paypal-payments' ),
'default' => null,
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_sandbox_validation_file' => array(
'title' => __( 'Apple Pay Sandbox Validation File', 'woocommerce-paypal-payments' ),
'type' => 'text',
'desc_tip' => true,
'description' => __( 'Paste here the validation file content', 'woocommerce-paypal-payments' ),
'default' => null,
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_type' => array(
'applepay_button_type' => array(
'title' => str_repeat( '&nbsp;', 6 ) . __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
@ -94,7 +118,7 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_color' => array(
'applepay_button_color' => array(
'title' => str_repeat( '&nbsp;', 6 ) . __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
@ -111,7 +135,7 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_language' => array(
'applepay_button_language' => array(
'title' => str_repeat( '&nbsp;', 6 ) . __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,

View file

@ -2,6 +2,8 @@ import ContextHandlerFactory from "./Context/ContextHandlerFactory";
import {createAppleErrors} from "./Helper/applePayError";
import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator";
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
class ApplepayButton {
@ -13,6 +15,7 @@ class ApplepayButton {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.paymentsClient = null;
this.form_saved = false;
this.contextHandler = ContextHandlerFactory.create(
this.context,
@ -42,14 +45,14 @@ class ApplepayButton {
if (isEligible) {
this.fetchTransactionInfo().then(() => {
const isSubscriptionProduct = this.ppcpConfig.data_client_id.has_subscriptions === true;
if(isSubscriptionProduct) {
if (isSubscriptionProduct) {
return;
}
this.addButton();
const id_minicart = "#apple-" + this.buttonConfig.button.mini_cart_wrapper;
const id = "#apple-" + this.buttonConfig.button.wrapper;
if(this.context === 'mini-cart') {
if (this.context === 'mini-cart') {
document.querySelector(id_minicart).addEventListener('click', (evt) => {
evt.preventDefault();
this.onButtonClick();
@ -60,10 +63,13 @@ class ApplepayButton {
this.onButtonClick();
});
}
// Listen for changes on any input within the WooCommerce checkout form
jQuery('form.checkout').on('change', 'input, select, textarea', () => {
this.fetchTransactionInfo();
});
});
}
console.log('[ApplePayButton] init done', this.buttonConfig.ajax_url);
}
async fetchTransactionInfo() {
this.transactionInfo = await this.contextHandler.transactionInfo();
@ -94,11 +100,12 @@ class ApplepayButton {
}
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
const wrapper_id = '#' + wrapper;
const syncButtonVisibility = () => {
const $ppcpButtonWrapper = jQuery(ppcpButtonWrapper);
setVisible(wrapper, $ppcpButtonWrapper.is(':visible'));
setEnabled(wrapper, !$ppcpButtonWrapper.hasClass('ppcp-disabled'));
setVisible(wrapper_id, $ppcpButtonWrapper.is(':visible'));
setEnabled(wrapper_id, !$ppcpButtonWrapper.hasClass('ppcp-disabled'));
}
jQuery(document).on('ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled', (ev, data) => {
@ -120,6 +127,7 @@ class ApplepayButton {
}
session.onvalidatemerchant = this.onvalidatemerchant(session);
session.onpaymentauthorized = this.onpaymentauthorized(session);
return session;
}
@ -129,19 +137,15 @@ class ApplepayButton {
* Add a Apple Pay purchase button
*/
addButton() {
console.log('[ApplePayButton] context', this.context);
const wrapper =
(this.context === 'mini-cart')
? this.buttonConfig.button.mini_cart_wrapper
: this.buttonConfig.button.wrapper;
console.log('[ApplePayButton] wrapper', wrapper)
const shape =
(this.context === 'mini-cart')
? this.ppcpConfig.button.mini_cart_style.shape
: this.ppcpConfig.button.style.shape;
const appleContainer = this.context === 'mini-cart' ? document.getElementById("applepay-container-minicart") : document.getElementById("applepay-container");
console.log('[ApplePayButton] shape', shape)
console.log('[ApplePayButton] container', appleContainer)
const type = this.buttonConfig.button.type;
const language = this.buttonConfig.button.lang;
const color = this.buttonConfig.button.color;
@ -150,7 +154,6 @@ console.log('[ApplePayButton] wrapper', wrapper)
jQuery('#' + wrapper).addClass('ppcp-button-' + shape);
jQuery(wrapper).append(appleContainer);
console.log('[ApplePayButton] addButton', wrapper, appleContainer);
}
//------------------------
@ -160,13 +163,58 @@ console.log('[ApplePayButton] wrapper', wrapper)
/**
* Show Apple Pay payment sheet when Apple Pay payment button is clicked
*/
onButtonClick() {
async onButtonClick() {
const paymentDataRequest = this.paymentDataRequest();
console.log('[ApplePayButton] onButtonClick: paymentDataRequest', paymentDataRequest, this.context);
// trigger woocommerce validation if we are in the checkout page
if (this.context === 'checkout') {
const checkoutFormSelector = 'form.woocommerce-checkout';
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
try {
const formData = new FormData(document.querySelector(checkoutFormSelector));
this.form_saved = Object.fromEntries(formData.entries());
this.update_request_data_with_form(paymentDataRequest);
} catch (error) {
console.error(error);
}
const session = this.applePaySession(paymentDataRequest)
console.log("session", session)
const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ?
new FormValidator(
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
PayPalCommerceGateway.ajax.validate_checkout.nonce,
) : null;
if (formValidator) {
try {
const errors = await formValidator.validate(document.querySelector(checkoutFormSelector));
if (errors.length > 0) {
errorHandler.messages(errors);
// fire WC event for other plugins
jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] );
// stop Apple Pay payment sheet from showing
session.abort();
return;
}
} catch (error) {
console.error(error);
}
}
return;
}
this.applePaySession(paymentDataRequest)
}
update_request_data_with_form(paymentDataRequest) {
paymentDataRequest.billingContact = this.fill_billing_contact(this.form_saved);
paymentDataRequest.applicationData = this.fill_application_data(this.form_saved);
if (!this.buttonConfig.product.needShipping) {
return;
}
paymentDataRequest.shippingContact = this.fill_shipping_contact(this.form_saved);
}
paymentDataRequest() {
const applepayConfig = this.applePayConfig
const buttonConfig = this.buttonConfig
@ -174,12 +222,9 @@ console.log('[ApplePayButton] wrapper', wrapper)
countryCode: applepayConfig.countryCode,
merchantCapabilities: applepayConfig.merchantCapabilities,
supportedNetworks: applepayConfig.supportedNetworks,
requiredShippingContactFields: ["name", "phone",
"email", "postalAddress"],
requiredBillingContactFields: ["name", "phone", "email",
"postalAddress"]
requiredShippingContactFields: ["postalAddress"],
requiredBillingContactFields: ["postalAddress"]
}
console.log('[ApplePayButton] paymentDataRequest', applepayConfig, buttonConfig);
const paymentDataRequest = Object.assign({}, baseRequest);
paymentDataRequest.currencyCode = buttonConfig.shop.currencyCode;
paymentDataRequest.total = {
@ -197,6 +242,7 @@ console.log('[ApplePayButton] wrapper', wrapper)
//------------------------
onvalidatemerchant(session) {
console.log("onvalidatemerchant")
return (applePayValidateMerchantEvent) => {
paypal.Applepay().validateMerchant({
validationUrl: applePayValidateMerchantEvent.validationURL
@ -234,7 +280,6 @@ console.log('[ApplePayButton] wrapper', wrapper)
onshippingmethodselected(session) {
const ajax_url = this.buttonConfig.ajax_url
console.log('[ApplePayButton] onshippingmethodselected');
return (event) => {
const data = this.getShippingMethodData(event);
jQuery.ajax({
@ -271,7 +316,7 @@ console.log('[ApplePayButton] wrapper', wrapper)
}
onshippingcontactselected(session) {
const ajax_url = this.buttonConfig.ajax_url
console.log('[ApplePayButton] onshippingcontactselected', ajax_url, session)
return (event) => {
const data = this.getShippingContactData(event);
console.log('shipping contact selected', data, event)
@ -514,6 +559,44 @@ console.log('[ApplePayButton] wrapper', wrapper)
return response;
}*/
fill_billing_contact(form_saved) {
return {
givenName: form_saved.billing_first_name ?? '',
familyName: form_saved.billing_last_name ?? '',
emailAddress: form_saved.billing_email ?? '',
phoneNumber: form_saved.billing_phone ?? '',
addressLines: [form_saved.billing_address_1, form_saved.billing_address_2],
locality: form_saved.billing_city ?? '',
postalCode: form_saved.billing_postcode ?? '',
countryCode: form_saved.billing_country ?? '',
administrativeArea: form_saved.billing_state ?? '',
}
}
fill_shipping_contact(form_saved) {
if (form_saved.shipping_first_name === "") {
return this.fill_billing_contact(form_saved)
}
return {
givenName: (form_saved?.shipping_first_name && form_saved.shipping_first_name !== "") ? form_saved.shipping_first_name : form_saved?.billing_first_name,
familyName: (form_saved?.shipping_last_name && form_saved.shipping_last_name !== "") ? form_saved.shipping_last_name : form_saved?.billing_last_name,
emailAddress: (form_saved?.shipping_email && form_saved.shipping_email !== "") ? form_saved.shipping_email : form_saved?.billing_email,
phoneNumber: (form_saved?.shipping_phone && form_saved.shipping_phone !== "") ? form_saved.shipping_phone : form_saved?.billing_phone,
addressLines: [form_saved.shipping_address_1 ?? '', form_saved.shipping_address_2 ?? ''],
locality: (form_saved?.shipping_city && form_saved.shipping_city !== "") ? form_saved.shipping_city : form_saved?.billing_city,
postalCode: (form_saved?.shipping_postcode && form_saved.shipping_postcode !== "") ? form_saved.shipping_postcode : form_saved?.billing_postcode,
countryCode: (form_saved?.shipping_country && form_saved.shipping_country !== "") ? form_saved.shipping_country : form_saved?.billing_country,
administrativeArea: (form_saved?.shipping_state && form_saved.shipping_state !== "") ? form_saved.shipping_state : form_saved?.billing_state,
}
}
fill_application_data(form_saved) {
const jsonString = JSON.stringify(form_saved);
let utf8Str = encodeURIComponent(jsonString).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode('0x' + p1);
});
return btoa(utf8Str);
}
}
export default ApplepayButton;

View file

@ -15,14 +15,28 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
use WooCommerce\PayPalCommerce\Applepay\Assets\DataToAppleButtonScripts;
use WooCommerce\PayPalCommerce\Applepay\Assets\BlocksPaymentMethod;
use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
'applepay.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'applepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency();
},
'applepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'applepay.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-apple-status-cache' );
},
'applepay.apple-product-status' => static function( ContainerInterface $container ): AppleProductStatus {
'applepay.apple-product-status' => static function( ContainerInterface $container ): AppleProductStatus {
return new AppleProductStatus(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
@ -30,18 +44,21 @@ return array(
$container->get( 'onboarding.state' )
);
},
'applepay.enabled' => static function ( ContainerInterface $container ): bool {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
/**
* If merchant isn't onboarded via /v1/customer/partner-referrals this returns false as the API call fails.
*/
return apply_filters( 'woocommerce_paypal_payments_applepay_product_status', $status->apple_is_active() );
'applepay.enabled' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_applepay_validate_product_status', false ) ) {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
/**
* If merchant isn't onboarded via /v1/customer/partner-referrals this returns false as the API call fails.
*/
return apply_filters( 'woocommerce_paypal_payments_applepay_product_status', $status->apple_is_active() );
}
return true;
},
'applepay.server_supported' => static function ( ContainerInterface $container ): bool {
'applepay.server_supported' => static function ( ContainerInterface $container ): bool {
return ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off';
},
'applepay.url' => static function ( ContainerInterface $container ): string {
'applepay.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
@ -51,13 +68,13 @@ return array(
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
return 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
},
'applepay.data_to_scripts' => static function ( ContainerInterface $container ): DataToAppleButtonScripts {
'applepay.data_to_scripts' => static function ( ContainerInterface $container ): DataToAppleButtonScripts {
return new DataToAppleButtonScripts( $container->get( 'applepay.sdk_script_url' ), $container->get( 'wcgateway.settings' ) );
},
'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton {
'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton {
return new ApplePayButton(
$container->get( 'wcgateway.settings' ),
@ -69,7 +86,7 @@ return array(
$container->get( 'wcgateway.settings.status' )
);
},
'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
return new BlocksPaymentMethod(
'ppcp-applepay',
$container->get( 'applepay.url' ),
@ -78,4 +95,25 @@ return array(
$container->get( 'blocks.method' )
);
},
/**
* The matrix which countries and currency combinations can be used for ApplePay.
*/
'applepay.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for ApplePay.
*/
return apply_filters(
'woocommerce_paypal_payments_applepay_supported_country_currency_matrix',
array(
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
)
);
},
);

File diff suppressed because one or more lines are too long

View file

@ -173,11 +173,19 @@ class DataToAppleButtonScripts {
'<div id="applepay-container">'
. $nonce
. '</div>';
$type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : '';
$color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : '';
$lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang );
return array(
'sdk_url' => $this->sdk_url,
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => array(
'needShipping' => $cart->needs_shipping(),

View file

@ -0,0 +1,67 @@
<?php
/**
* ApmApplies helper.
*
* @package WooCommerce\PayPalCommerce\ApplePay\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay\Helper;
/**
* Class ApmApplies
*/
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for DCC.
*
* @var array
*/
private $allowed_country_currency_matrix;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* ApmApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether ApplePay can be used in the current country and the current currency used.
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
}
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay;
use WooCommerce\PayPalCommerce\Googlepay\Helper\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
return array(
@ -31,6 +32,9 @@ return array(
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
};
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
return $insert_after(
$fields,
'allow_card_button_gateway',
@ -52,20 +56,16 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-handlers' => wp_json_encode(
'data-ppcp-display' => wp_json_encode(
array(
array(
'handler' => 'SubElementsHandler',
'options' => array(
'values' => array( '1' ),
'elements' => array(
'#field-googlepay_button_color',
'#field-googlepay_button_type',
'#field-googlepay_button_language',
'#field-googlepay_button_shipping_enabled',
),
),
),
$display_manager
->rule()
->condition_element( 'googlepay_button_enabled', '1' )
->action_visible( 'googlepay_button_type' )
->action_visible( 'googlepay_button_color' )
->action_visible( 'googlepay_button_language' )
->action_visible( 'googlepay_button_shipping_enabled' )
->to_array(),
)
),
),

View file

@ -0,0 +1,11 @@
.ppcp-field-hidden {
display: none !important;
}
.ppcp-field-disabled {
cursor: not-allowed;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
opacity: 0.5;
}

View file

@ -1,51 +0,0 @@
class SubElementsHandler {
constructor(element, options) {
const fieldSelector = 'input, select, textarea';
this.element = element;
this.values = options.values;
this.elements = options.elements;
this.elementsSelector = this.elements.join(',');
this.input = jQuery(this.element).is(fieldSelector)
? this.element
: jQuery(this.element).find(fieldSelector).get(0);
this.updateElementsVisibility();
jQuery(this.input).change(() => {
this.updateElementsVisibility();
});
}
updateElementsVisibility() {
const $elements = jQuery(this.elementsSelector);
let value = this.getValue(this.input);
value = (value !== null ? value.toString() : value);
if (this.values.indexOf(value) !== -1) {
$elements.removeClass('hide');
} else {
$elements.addClass('hide');
}
}
getValue(element) {
const $el = jQuery(element);
if ($el.is(':checkbox') || $el.is(':radio')) {
if ($el.is(':checked')) {
return $el.val();
} else {
return null;
}
} else {
return $el.val();
}
}
}
export default SubElementsHandler;

View file

@ -1,10 +1,28 @@
import DisplayManager from "./common/display-manager/DisplayManager";
import moveWrappedElements from "./common/wrapped-elements";
document.addEventListener(
'DOMContentLoaded',
() => {
// Wait for current execution context to end.
setTimeout(function () {
moveWrappedElements();
}, 0);
// Initialize DisplayManager.
const displayManager = new DisplayManager();
jQuery( '*[data-ppcp-display]' ).each( (index, el) => {
const rules = jQuery(el).data('ppcpDisplay');
console.log('rules', rules);
for (const rule of rules) {
displayManager.addRule(rule);
}
});
displayManager.register();
}
);

View file

@ -0,0 +1,14 @@
import ElementAction from "./action/ElementAction";
class ActionFactory {
static make(actionConfig) {
switch (actionConfig.type) {
case 'element':
return new ElementAction(actionConfig);
}
throw new Error('[ActionFactory] Unknown action: ' + actionConfig.type);
}
}
export default ActionFactory;

View file

@ -0,0 +1,17 @@
import ElementCondition from "./condition/ElementCondition";
import BoolCondition from "./condition/BoolCondition";
class ConditionFactory {
static make(conditionConfig, triggerUpdate) {
switch (conditionConfig.type) {
case 'element':
return new ElementCondition(conditionConfig, triggerUpdate);
case 'bool':
return new BoolCondition(conditionConfig, triggerUpdate);
}
throw new Error('[ConditionFactory] Unknown condition: ' + conditionConfig.type);
}
}
export default ConditionFactory;

View file

@ -0,0 +1,32 @@
import Rule from "./Rule";
class DisplayManager {
constructor() {
this.rules = {};
this.ruleStatus = {}; // The current status for each rule. Maybe not necessary, for now just for logging.
document.ppcpDisplayManagerLog = () => {
console.log('DisplayManager', this);
}
}
addRule(ruleConfig) {
const updateStatus = () => {
this.ruleStatus[ruleConfig.key] = this.rules[ruleConfig.key].status;
console.log('ruleStatus', this.ruleStatus);
}
this.rules[ruleConfig.key] = new Rule(ruleConfig, updateStatus.bind(this));
console.log('Rule', this.rules[ruleConfig.key]);
}
register() {
this.ruleStatus = {};
for (const [key, rule] of Object.entries(this.rules)) {
rule.register();
}
}
}
export default DisplayManager;

View file

@ -0,0 +1,68 @@
import ConditionFactory from "./ConditionFactory";
import ActionFactory from "./ActionFactory";
class Rule {
constructor(config, triggerUpdate) {
this.config = config;
this.conditions = {};
this.actions = {};
this.triggerUpdate = triggerUpdate;
this.status = null;
const updateStatus = this.updateStatus.bind(this);
for (const conditionConfig of this.config.conditions) {
const condition = ConditionFactory.make(conditionConfig, updateStatus);
this.conditions[condition.key] = condition;
console.log('Condition', condition);
}
for (const actionConfig of this.config.actions) {
const action = ActionFactory.make(actionConfig);
this.actions[action.key] = action;
console.log('Action', action);
}
}
get key() {
return this.config.key;
}
updateStatus(forceRunActions = false) {
let status = true;
for (const [key, condition] of Object.entries(this.conditions)) {
status &= condition.status;
}
if (status !== this.status) {
this.status = status;
this.triggerUpdate();
this.runActions();
} else if (forceRunActions) {
this.runActions();
}
}
runActions() {
for (const [key, action] of Object.entries(this.actions)) {
action.run(this.status);
}
}
register() {
for (const [key, condition] of Object.entries(this.conditions)) {
condition.register(this.updateStatus.bind(this));
}
for (const [key, action] of Object.entries(this.actions)) {
action.register();
}
this.updateStatus(true);
}
}
export default Rule;

View file

@ -0,0 +1,21 @@
class BaseAction {
constructor(config) {
this.config = config;
}
get key() {
return this.config.key;
}
register() {
// To override.
}
run(status) {
// To override.
}
}
export default BaseAction;

View file

@ -0,0 +1,35 @@
import BaseAction from "./BaseAction";
class ElementAction extends BaseAction {
run(status) {
if (status) {
if (this.config.action === 'visible') {
jQuery(this.config.selector).removeClass('ppcp-field-hidden');
}
if (this.config.action === 'enable') {
jQuery(this.config.selector).removeClass('ppcp-field-disabled')
.off('mouseup')
.find('> *')
.css('pointer-events', '');
}
} else {
if (this.config.action === 'visible') {
jQuery(this.config.selector).addClass('ppcp-field-hidden');
}
if (this.config.action === 'enable') {
jQuery(this.config.selector).addClass('ppcp-field-disabled')
.on('mouseup', function(event) {
event.stopImmediatePropagation();
})
.find('> *')
.css('pointer-events', 'none');
}
}
}
}
export default ElementAction;

View file

@ -0,0 +1,19 @@
class BaseCondition {
constructor(config, triggerUpdate) {
this.config = config;
this.status = false;
this.triggerUpdate = triggerUpdate;
}
get key() {
return this.config.key;
}
register() {
// To override.
}
}
export default BaseCondition;

View file

@ -0,0 +1,15 @@
import BaseCondition from "./BaseCondition";
class BoolCondition extends BaseCondition {
register() {
this.status = this.check();
}
check() {
return !! this.config.value;
}
}
export default BoolCondition;

View file

@ -0,0 +1,27 @@
import BaseCondition from "./BaseCondition";
import {inputValue} from "../../../helper/form";
class ElementCondition extends BaseCondition {
register() {
jQuery(document).on('change', this.config.selector, () => {
const status = this.check();
if (status !== this.status) {
this.status = status;
this.triggerUpdate();
}
});
this.status = this.check();
}
check() {
let value = inputValue(this.config.selector);
value = (value !== null ? value.toString() : value);
return this.config.value === value;
}
}
export default ElementCondition;

View file

@ -4,7 +4,6 @@ import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Rendere
import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer";
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import SubElementsHandler from "./SettingsHandler/SubElementsHandler";
document.addEventListener(
'DOMContentLoaded',
@ -308,16 +307,5 @@ document.addEventListener(
createButtonPreview(() => getButtonDefaultSettings('#ppcpPayLaterButtonPreview'));
});
}
// Generic behaviours, can be moved to common.js once it's on trunk branch.
jQuery( '*[data-ppcp-handlers]' ).each( (index, el) => {
const handlers = jQuery(el).data('ppcpHandlers');
for (const handlerConfig of handlers) {
new {
SubElementsHandler: SubElementsHandler
}[handlerConfig.handler](el, handlerConfig.options)
}
});
}
);

View file

@ -0,0 +1,13 @@
export const inputValue = (element) => {
const $el = jQuery(element);
if ($el.is(':checkbox') || $el.is(':radio')) {
if ($el.is(':checked')) {
return $el.val();
} else {
return null;
}
} else {
return $el.val();
}
}

View file

@ -11,21 +11,18 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
@ -35,6 +32,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset;
use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use WooCommerce\PayPalCommerce\WcGateway\Cli\SettingsCommand;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSessionId;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSourceWebsiteId;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -42,15 +42,13 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSessionId;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSourceWebsiteId;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PaymentSourceFactory;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoice;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
@ -933,6 +931,7 @@ return array(
'wcgateway.extra-funding-sources' => static function( ContainerInterface $container ): array {
return array(
'googlepay' => _x( 'Google Pay', 'Name of payment method', 'woocommerce-paypal-payments' ),
'applepay' => _x( 'Apple Pay', 'Name of payment method', 'woocommerce-paypal-payments' ),
);
},
@ -1399,4 +1398,10 @@ return array(
$container->get( 'wcgateway.settings' )
);
},
'wcgateway.display-manager' => SingletonDecorator::make(
static function( ContainerInterface $container ): DisplayManager {
$settings = $container->get( 'wcgateway.settings' );
return new DisplayManager( $settings );
}
),
);

View file

@ -229,6 +229,13 @@ class SettingsPageAssets {
* Register assets for PayPal admin pages.
*/
private function register_admin_assets(): void {
wp_enqueue_style(
'ppcp-admin-common',
trailingslashit( $this->module_url ) . 'assets/css/common.css',
array(),
$this->version
);
wp_enqueue_script(
'ppcp-admin-common',
trailingslashit( $this->module_url ) . 'assets/js/common.js',

View file

@ -0,0 +1,60 @@
<?php
/**
* Helper to manage the field display behaviour.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Helper;
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* DisplayManager class.
*/
class DisplayManager {
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The rules.
*
* @var array
*/
protected $rules = array();
/**
* FieldDisplayManager constructor.
*
* @param Settings $settings The settings.
* @return void
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* Creates and returns a rule.
*
* @param string|null $key The rule key.
* @return DisplayRule
*/
public function rule( string $key = null ): DisplayRule {
if ( null === $key ) {
$key = '_rule_' . ( (string) count( $this->rules ) );
}
$rule = new DisplayRule( $key, $this->settings );
$this->rules[ $key ] = $rule;
return $rule;
}
}

View file

@ -0,0 +1,262 @@
<?php
/**
* Element used by field display manager.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Helper;
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* DisplayRule class.
*/
class DisplayRule {
const CONDITION_TYPE_ELEMENT = 'element';
const CONDITION_TYPE_BOOL = 'bool';
const CONDITION_OPERATION_EQUALS = 'equals';
const CONDITION_OPERATION_NOT_EQUALS = 'not_equals';
const CONDITION_OPERATION_IN = 'in';
const CONDITION_OPERATION_NOT_IN = 'not_in';
const CONDITION_OPERATION_EMPTY = 'empty';
const CONDITION_OPERATION_NOT_EMPTY = 'not_empty';
const ACTION_TYPE_ELEMENT = 'element';
const ACTION_VISIBLE = 'visible';
const ACTION_ENABLE = 'enable';
/**
* The element selector.
*
* @var string
*/
protected $key;
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The conditions of this rule.
*
* @var array
*/
protected $conditions = array();
/**
* The actions of this rule.
*
* @var array
*/
protected $actions = array();
/**
* Indicates if this class should add selector prefixes.
*
* @var bool
*/
protected $add_selector_prefixes = true;
/**
* FieldDisplayElement constructor.
*
* @param string $key The rule key.
* @param Settings $settings The settings.
*/
public function __construct( string $key, Settings $settings ) {
$this->key = $key;
$this->settings = $settings;
}
/**
* Adds a condition related to an HTML element.
*
* @param string $selector The condition selector.
* @param mixed $value The value to compare against.
* @param string $operation The condition operation (ex: equals, differs, in, not_empty, empty).
* @return self
*/
public function condition_element( string $selector, $value, string $operation = self::CONDITION_OPERATION_EQUALS ): self {
$this->add_condition(
array(
'type' => self::CONDITION_TYPE_ELEMENT,
'selector' => $selector,
'operation' => $operation,
'value' => $value,
)
);
return $this;
}
/**
* Adds a condition related to a bool check.
*
* @param bool $value The value to enable / disable the condition.
* @return self
*/
public function condition_is_true( bool $value ): self {
$this->add_condition(
array(
'type' => self::CONDITION_TYPE_BOOL,
'value' => $value,
)
);
return $this;
}
/**
* Adds a condition related to the settings.
*
* @param string $settings_key The settings key.
* @param mixed $value The value to compare against.
* @param string $operation The condition operation (ex: equals, differs, in, not_empty, empty).
* @return self
*/
public function condition_is_settings( string $settings_key, $value, string $operation = self::CONDITION_OPERATION_EQUALS ): self {
$settings_value = null;
if ( $this->settings->has( $settings_key ) ) {
$settings_value = $this->settings->get( $settings_key );
}
$this->condition_is_true( $this->resolve_operation( $settings_value, $value, $operation ) );
return $this;
}
/**
* Adds a condition to show/hide the element.
*
* @param string $selector The condition selector.
*/
public function action_visible( string $selector ): self {
$this->add_action(
array(
'type' => self::ACTION_TYPE_ELEMENT,
'selector' => $selector,
'action' => self::ACTION_VISIBLE,
)
);
return $this;
}
/**
* Adds a condition to enable/disable the element.
*
* @param string $selector The condition selector.
*/
public function action_enable( string $selector ): self {
$this->add_action(
array(
'type' => self::ACTION_TYPE_ELEMENT,
'selector' => $selector,
'action' => self::ACTION_ENABLE,
)
);
return $this;
}
/**
* Adds a condition to the rule.
*
* @param array $options The condition options.
* @return void
*/
private function add_condition( array $options ): void {
if ( $this->add_selector_prefixes && isset( $options['selector'] ) ) {
$options['selector'] = '#ppcp-' . $options['selector']; // Refers to the input.
}
if ( ! isset( $options['key'] ) ) {
$options['key'] = '_condition_' . ( (string) count( $this->conditions ) );
}
$this->conditions[] = $options;
}
/**
* Adds an action to do.
*
* @param array $options The action options.
* @return void
*/
private function add_action( array $options ): void {
if ( $this->add_selector_prefixes && isset( $options['selector'] ) ) {
$options['selector'] = '#field-' . $options['selector']; // Refers to the whole field.
}
if ( ! isset( $options['key'] ) ) {
$options['key'] = '_action_' . ( (string) count( $this->actions ) );
}
$this->actions[] = $options;
}
/**
* Set if selector prefixes like, "#ppcp-" or "#field-" should be added to condition or action selectors.
*
* @param bool $add_selector_prefixes If should add prefixes.
* @return self
*/
public function should_add_selector_prefixes( bool $add_selector_prefixes = true ): self {
$this->add_selector_prefixes = $add_selector_prefixes;
return $this;
}
/**
* Adds a condition related to the settings.
*
* @param mixed $value_1 The value 1.
* @param mixed $value_2 The value 2.
* @param string $operation The condition operation (ex: equals, differs, in, not_empty, empty).
* @return bool
*/
private function resolve_operation( $value_1, $value_2, string $operation ): bool {
switch ( $operation ) {
case self::CONDITION_OPERATION_EQUALS:
return $value_1 === $value_2;
case self::CONDITION_OPERATION_NOT_EQUALS:
return $value_1 !== $value_2;
case self::CONDITION_OPERATION_IN:
return in_array( $value_1, $value_2, true );
case self::CONDITION_OPERATION_NOT_IN:
return ! in_array( $value_1, $value_2, true );
case self::CONDITION_OPERATION_EMPTY:
return empty( $value_1 );
case self::CONDITION_OPERATION_NOT_EMPTY:
return ! empty( $value_1 );
}
return false;
}
/**
* Returns array representation.
*
* @return array
*/
public function to_array(): array {
return array(
'key' => $this->key,
'conditions' => $this->conditions,
'actions' => $this->actions,
);
}
/**
* Returns JSON representation.
*
* @return string
*/
public function json(): string {
return wp_json_encode( $this->to_array() ) ?: '';
}
}

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
return function ( ContainerInterface $container, array $fields ): array {
@ -39,6 +40,9 @@ return function ( ContainerInterface $container, array $fields ): array {
$module_url = $container->get( 'wcgateway.url' );
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
$connection_fields = array(
'ppcp_onboarading_header' => array(
'type' => 'ppcp-text',
@ -504,15 +508,13 @@ return function ( ContainerInterface $container, array $fields ): array {
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
'custom_attributes' => array(
'data-ppcp-handlers' => wp_json_encode(
'data-ppcp-display' => wp_json_encode(
array(
array(
'handler' => 'SubElementsHandler',
'options' => array(
'values' => array( PurchaseUnitSanitizer::MODE_EXTRA_LINE ),
'elements' => array( '#field-subtotal_mismatch_line_name' ),
),
),
$display_manager
->rule()
->condition_element( 'subtotal_mismatch_behavior', PurchaseUnitSanitizer::MODE_EXTRA_LINE )
->action_visible( 'subtotal_mismatch_line_name' )
->to_array(),
)
),
),

View file

@ -11,6 +11,7 @@ module.exports = {
'fraudnet': path.resolve('./resources/js/fraudnet.js'),
'oxxo': path.resolve('./resources/js/oxxo.js'),
'gateway-settings-style': path.resolve('./resources/css/gateway-settings.scss'),
'common-style': path.resolve('./resources/css/common.scss'),
},
output: {
path: path.resolve(__dirname, 'assets/'),