🔀 Merge branch 'trunk' into PCP-3179-apple-pay-google-pay-buttons-no-longer-visible-in-standard-payments-button-previews-after-moving-them-to-advanced-card-processing-tab

This commit is contained in:
Philipp Stracker 2024-06-05 18:43:47 +02:00
commit 202f308850
No known key found for this signature in database
38 changed files with 820 additions and 251 deletions

View file

@ -1,5 +1,18 @@
*** Changelog ***
= 2.8.0 - xxxx-xx-xx =
* Fix - Calculate totals after adding shipping to include taxes #2296
* Fix - Package tracking integration throws error in 2.7.1 #2289
* Fix - Make PayPal Subscription products unique in cart #2265
* Fix - PayPal declares subscription support when merchant not enabled for Reference Transactions #2282
* Fix - Google Pay and Apple Pay Settings button from Connection tab have wrong links #2273
* Fix - Smart Buttons in Block Checkout not respecting the location setting (2830) #2278
* Fix - Disable Pay Upon Invoice if billing/shipping country not set #2281
* Enhancement - Enable shipping callback for WC subscriptions #2259
* Enhancement - Disable the shipping callback for "venmo" when vaulting is active #2269
* Enhancement - Improve "Could not retrieve order" error message #2271
* Enhancement - Add block Checkout compatibility to Advanced Card Processing #2246
= 2.7.1 - 2024-05-28 =
* Fix - Ensure package tracking data is sent to original PayPal transaction #2180
* Fix - Set the 'Woo_PPCP' as a default value for data-partner-attribution-id #2188

View file

@ -73,6 +73,7 @@ class SdkClientToken {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$domain = wp_unslash( $_SERVER['HTTP_HOST'] ?? '' );
$domain = preg_replace( '/^www\./', '', $domain );
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=client_token&intent=sdk_init&domains[]=' . $domain;

View file

@ -50,9 +50,7 @@ class ShippingOptionFactory {
$cart->calculate_shipping();
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() );
if ( ! is_array( $chosen_shipping_methods ) ) {
$chosen_shipping_methods = array();
}
$chosen_shipping_method = $chosen_shipping_methods[0] ?? false;
$packages = WC()->shipping()->get_packages();
$options = array();
@ -62,11 +60,10 @@ class ShippingOptionFactory {
if ( ! $rate instanceof \WC_Shipping_Rate ) {
continue;
}
$options[] = new ShippingOption(
$rate->get_id(),
$rate->get_label(),
in_array( $rate->get_id(), $chosen_shipping_methods, true ),
$rate->get_id() === $chosen_shipping_method,
new Money(
(float) $rate->get_cost(),
get_woocommerce_currency()

View file

@ -148,14 +148,8 @@ return array(
'axo_privacy' => array(
'title' => __( 'Privacy', 'woocommerce-paypal-payments' ),
'type' => 'select',
'label' => __(
'This setting will control whether Fastlane branding is shown by email field.
<p class="description">PayPal powers this accelerated checkout solution from Fastlane. Since you\'ll share consumers\' email addresses with PayPal, please consult your legal advisors on the apropriate privacy setting for your business.</p>',
'woocommerce-paypal-payments'
),
'desc_tip' => true,
'description' => __(
'This setting will control whether Fastlane branding is shown by email field.',
'PayPal powers this accelerated checkout solution from Fastlane. Since you\'ll share consumers\' email address with PayPal, please consult your legal advisors on the appropriate privacy setting for your business.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
@ -168,12 +162,14 @@ return array(
'requirements' => array( 'axo' ),
),
'axo_name_on_card' => array(
'title' => __( 'Display Name on Card', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'title' => __( 'Cardholder Name', 'woocommerce-paypal-payments' ),
'type' => 'select',
'default' => 'yes',
'options' => PropertiesDictionary::cardholder_name_options(),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'label' => __( 'Enable this to display the "Name on Card" field for new Fastlane buyers.', 'woocommerce-paypal-payments' ),
'input_class' => array( 'wc-enhanced-select' ),
'description' => __( 'This setting will control whether or not the cardholder name is displayed in the card field\'s UI.', 'woocommerce-paypal-payments' ),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array( 'dcc', 'axo' ),
'requirements' => array( 'axo' ),
@ -196,7 +192,7 @@ return array(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'Leave the default styling, or customize how Fastlane looks on your website. %1$sSee PayPal\'s developer docs%2$s for info',
'Leave the default styling, or customize how Fastlane looks on your website. Styles that don\'t meet accessibility guidelines will revert to the defaults. See %1$sPayPal\'s developer docs%2$s for info.',
'woocommerce-paypal-payments'
),
'<a href="https://www.paypal.com/us/fastlane" target="_blank">',
@ -236,18 +232,6 @@ return array(
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_primary_color' => array(
'title' => __( 'Primary Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#0057F',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_error_color' => array(
'title' => __( 'Error Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
@ -308,6 +292,18 @@ return array(
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_primary_color' => array(
'title' => __( 'Primary Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#0057FF',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_heading' => array(
'heading' => __( 'Input Settings', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',

View file

@ -1,6 +1,15 @@
.ppcp-axo-watermark-container {
max-width: 200px;
margin-top: 10px;
position: relative;
&.loader:before {
height: 12px;
width: 12px;
margin-left: -6px;
margin-top: -6px;
left: 12px;
}
}
.ppcp-axo-payment-container {
@ -28,6 +37,7 @@
.ppcp-axo-customer-details {
margin-bottom: 40px;
position: relative;
}
.axo-checkout-header-section {
@ -44,6 +54,31 @@
padding: 0.6em 1em;
}
.ppcp-axo-watermark-loading {
min-height: 12px;
}
.ppcp-axo-overlay,
.ppcp-axo-watermark-loading:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
content: '';
}
.ppcp-axo-loading .col-1 {
position: relative;
opacity: 0.9;
transition: opacity 0.5s ease;
}
#payment .payment_methods li label[for="payment_method_ppcp-axo-gateway"] {
img {
float: none;

View file

@ -24,7 +24,8 @@ class AxoManager {
active: false,
validEmail: false,
hasProfile: false,
useEmailWidget: this.useEmailWidget()
useEmailWidget: this.useEmailWidget(),
hasCard: false,
};
this.data = {
@ -59,7 +60,6 @@ class AxoManager {
}
document.axoDebugObject = () => {
console.log(this);
return this;
}
@ -156,6 +156,7 @@ class AxoManager {
this.el.showGatewaySelectionLink.on('click', async () => {
this.hideGatewaySelection = false;
this.$('.wc_payment_methods label').show();
this.$('.wc_payment_methods input').show();
this.cardView.refresh();
});
@ -164,9 +165,8 @@ class AxoManager {
this.$('form.woocommerce-checkout input').on('keydown', async (ev) => {
if(ev.key === 'Enter' && getCurrentPaymentMethod() === 'ppcp-axo-gateway' ) {
ev.preventDefault();
log('Enter key attempt');
log('emailInput', this.emailInput.value);
log('this.lastEmailCheckedIdentity', this.lastEmailCheckedIdentity);
log(`Enter key attempt - emailInput: ${this.emailInput.value}`);
log(`this.lastEmailCheckedIdentity: ${this.lastEmailCheckedIdentity}`);
if (this.emailInput && this.lastEmailCheckedIdentity !== this.emailInput.value) {
await this.onChangeEmail();
}
@ -175,7 +175,7 @@ class AxoManager {
// Clear last email checked identity when email field is focused.
this.$('#billing_email_field input').on('focus', (ev) => {
log('Clear the last email checked:', this.lastEmailCheckedIdentity);
log(`Clear the last email checked: ${this.lastEmailCheckedIdentity}`);
this.lastEmailCheckedIdentity = '';
});
@ -212,7 +212,7 @@ class AxoManager {
this.status.hasProfile
);
log('Scenario', scenario);
log(`Scenario: ${JSON.stringify(scenario)}`);
// Reset some elements to a default status.
this.el.watermarkContainer.hide();
@ -231,6 +231,7 @@ class AxoManager {
if (scenario.defaultFormFields) {
this.el.customerDetails.show();
this.toggleLoaderAndOverlay(this.el.customerDetails, 'loader', 'ppcp-axo-overlay');
} else {
this.el.customerDetails.hide();
}
@ -248,7 +249,6 @@ class AxoManager {
this.$(this.el.fieldBillingEmail.selector).append(
this.$(this.el.watermarkContainer.selector)
);
} else {
this.el.emailWidgetContainer.hide();
if (!scenario.defaultEmailField) {
@ -257,12 +257,14 @@ class AxoManager {
}
if (scenario.axoProfileViews) {
this.el.billingAddressContainer.hide();
this.shippingView.activate();
this.billingView.activate();
this.cardView.activate();
if (this.status.hasCard) {
this.billingView.activate();
}
// Move watermark to after shipping.
this.$(this.el.shippingAddressContainer.selector).after(
this.$(this.el.watermarkContainer.selector)
@ -372,7 +374,7 @@ class AxoManager {
setStatus(key, value) {
this.status[key] = value;
log('Status updated', JSON.parse(JSON.stringify(this.status)));
log(`Status updated: ${JSON.stringify(this.status)}`);
document.dispatchEvent(new CustomEvent("axo_status_updated", {detail: this.status}));
@ -384,9 +386,8 @@ class AxoManager {
this.initFastlane();
this.setStatus('active', true);
log('Attempt on activation');
log('emailInput', this.emailInput.value);
log('this.lastEmailCheckedIdentity', this.lastEmailCheckedIdentity);
log(`Attempt on activation - emailInput: ${this.emailInput.value}`);
log(`this.lastEmailCheckedIdentity: ${this.lastEmailCheckedIdentity}`);
if (this.emailInput && this.lastEmailCheckedIdentity !== this.emailInput.value) {
this.onChangeEmail();
}
@ -496,6 +497,8 @@ class AxoManager {
(await this.fastlane.FastlaneWatermarkComponent({
includeAdditionalInfo
})).render(this.el.watermarkContainer.selector);
this.toggleWatermarkLoading(this.el.watermarkContainer, 'ppcp-axo-watermark-loading', 'loader');
}
watchEmail() {
@ -506,17 +509,15 @@ class AxoManager {
} else {
this.emailInput.addEventListener('change', async ()=> {
log('Change event attempt');
log('emailInput', this.emailInput.value);
log('this.lastEmailCheckedIdentity', this.lastEmailCheckedIdentity);
log(`Change event attempt - emailInput: ${this.emailInput.value}`);
log(`this.lastEmailCheckedIdentity: ${this.lastEmailCheckedIdentity}`);
if (this.emailInput && this.lastEmailCheckedIdentity !== this.emailInput.value) {
this.onChangeEmail();
}
});
log('Last, this.emailInput.value attempt');
log('emailInput', this.emailInput.value);
log('this.lastEmailCheckedIdentity', this.lastEmailCheckedIdentity);
log(`Last, this.emailInput.value attempt - emailInput: ${this.emailInput.value}`);
log(`this.lastEmailCheckedIdentity: ${this.lastEmailCheckedIdentity}`);
if (this.emailInput.value) {
this.onChangeEmail();
}
@ -536,7 +537,7 @@ class AxoManager {
return;
}
log('Email changed: ' + (this.emailInput ? this.emailInput.value : '<empty>'));
log(`Email changed: ${this.emailInput ? this.emailInput.value : '<empty>'}`);
this.$(this.el.paymentContainer.selector + '-detail').html('');
this.$(this.el.paymentContainer.selector + '-form').html('');
@ -565,12 +566,16 @@ class AxoManager {
page_type: 'checkout'
});
this.disableGatewaySelection();
await this.lookupCustomerByEmail();
this.enableGatewaySelection();
}
async lookupCustomerByEmail() {
const lookupResponse = await this.fastlane.identity.lookupCustomerByEmail(this.emailInput.value);
log(`lookupCustomerByEmail: ${JSON.stringify(lookupResponse)}`);
if (lookupResponse.customerContextId) {
// Email is associated with a Connect profile or a PayPal member.
// Authenticate the customer to get access to their profile.
@ -578,18 +583,24 @@ class AxoManager {
const authResponse = await this.fastlane.identity.triggerAuthenticationFlow(lookupResponse.customerContextId);
log('AuthResponse', authResponse);
log(`AuthResponse - triggerAuthenticationFlow: ${JSON.stringify(authResponse)}`);
if (authResponse.authenticationState === 'succeeded') {
log(JSON.stringify(authResponse));
const shippingData = authResponse.profileData.shippingAddress;
if(shippingData) {
if (shippingData) {
this.setShipping(shippingData);
}
if (authResponse.profileData.card) {
this.setStatus('hasCard', true);
} else {
this.cardComponent = (await this.fastlane.FastlaneCardComponent(
this.cardComponentData()
)).render(this.el.paymentContainer.selector + '-form');
}
const cardBillingAddress = authResponse.profileData?.card?.paymentSource?.card?.billingAddress;
if(cardBillingAddress) {
if (cardBillingAddress) {
this.setCard(authResponse.profileData.card);
const billingData = {
@ -608,6 +619,7 @@ class AxoManager {
this.hideGatewaySelection = true;
this.$('.wc_payment_methods label').hide();
this.$('.wc_payment_methods input').hide();
await this.renderWatermark(false);
@ -644,6 +656,14 @@ class AxoManager {
}
}
disableGatewaySelection() {
this.$('.wc_payment_methods input').prop('disabled', true);
}
enableGatewaySelection() {
this.$('.wc_payment_methods input').prop('disabled', false);
}
clearData() {
this.data = {
email: null,
@ -672,7 +692,7 @@ class AxoManager {
// TODO: validate data.
if (this.data.card) { // Ryan flow
log('Ryan flow.');
log('Starting Ryan flow.');
this.$('#ship-to-different-address-checkbox').prop('checked', 'checked');
@ -683,20 +703,23 @@ class AxoManager {
this.ensureBillingPhoneNumber(data);
log(`Ryan flow - submitted nonce: ${this.data.card.id}` )
this.submit(this.data.card.id, data);
} else { // Gary flow
log('Gary flow.');
log('Starting Gary flow.');
try {
this.cardComponent.getPaymentToken(
this.tokenizeData()
).then((response) => {
log(`Gary flow - submitted nonce: ${response.id}` )
this.submit(response.id);
});
} catch (e) {
log('Error tokenizing.');
alert('Error tokenizing data.');
log(`Error tokenizing data. ${e.message}`, 'error');
}
}
}
@ -714,7 +737,7 @@ class AxoManager {
tokenizeData() {
return {
name: {
cardholderName: {
fullName: this.billingView.fullName()
},
billingAddress: {
@ -776,7 +799,9 @@ class AxoManager {
scrollTop: $notices.offset().top
}, 500);
}
console.error('Failure:', responseData);
log(`Error sending checkout form. ${responseData}`, 'error');
this.hideLoading();
return;
}
@ -785,7 +810,8 @@ class AxoManager {
}
})
.catch(error => {
console.error('Error:', error);
log(`Error sending checkout form. ${error.message}`, 'error');
this.hideLoading();
});
@ -840,6 +866,28 @@ class AxoManager {
data.billing_phone = phone;
}
}
toggleLoaderAndOverlay(element, loaderClass, overlayClass) {
const loader = document.querySelector(`${element.selector} .${loaderClass}`);
const overlay = document.querySelector(`${element.selector} .${overlayClass}`);
if (loader) {
loader.classList.toggle(loaderClass);
}
if (overlay) {
overlay.classList.toggle(overlayClass);
}
}
toggleWatermarkLoading(container, loadingClass, loaderClass) {
const watermarkLoading = document.querySelector(`${container.selector}.${loadingClass}`);
const watermarkLoader = document.querySelector(`${container.selector}.${loaderClass}`);
if (watermarkLoading) {
watermarkLoading.classList.toggle(loadingClass);
}
if (watermarkLoader) {
watermarkLoader.classList.toggle(loaderClass);
}
}
}
export default AxoManager;

View file

@ -20,7 +20,7 @@ class DomElementCollection {
this.watermarkContainer = new DomElement({
id: 'ppcp-axo-watermark-container',
selector: '#ppcp-axo-watermark-container',
className: 'ppcp-axo-watermark-container'
className: 'ppcp-axo-watermark-container ppcp-axo-watermark-loading loader'
});
this.customerDetails = new DomElement({

View file

@ -1,4 +1,26 @@
export function log(message, level = 'info') {
const endpoint = window.wc_ppcp_axo?.ajax?.frontend_logger?.endpoint;
if(!endpoint) {
return;
}
export function log(...args) {
//console.log('[AXO] ', ...args);
fetch(endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: window.wc_ppcp_axo.ajax.frontend_logger.nonce,
log: {
message,
level,
}
})
}).then(() => {
switch (level) {
case 'error':
console.error(`[AXO] ${message}`);
break;
default:
console.log(`[AXO] ${message}`);
}
});
}

View file

@ -34,22 +34,7 @@ class BillingView {
</div>
`;
}
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-header-section">
<h3>Billing</h3>
<a href="javascript:void(0)" ${this.el.changeBillingAddressLink.attributes}>Edit</a>
</div>
<div>${data.value('email')}</div>
<div>${data.value('company')}</div>
<div>${data.value('firstName')} ${data.value('lastName')}</div>
<div>${data.value('street1')}</div>
<div>${data.value('street2')}</div>
<div>${data.value('postCode')} ${data.value('city')}</div>
<div>${valueOfSelect('#billing_state', data.value('stateCode'))}</div>
<div>${valueOfSelect('#billing_country', data.value('countryCode'))}</div>
</div>
`;
return '';
},
fields: {
email: {

View file

@ -20,10 +20,6 @@ class CardView {
if (data.isEmpty()) {
return `
<div style="margin-bottom: 20px; text-align: center;">
<div style="border:2px solid #cccccc; border-radius: 10px; padding: 26px 20px; margin-bottom: 20px; background-color:#f6f6f6">
<div>Please fill in your card details.</div>
</div>
<h4><a href="javascript:void(0)" ${this.el.changeCardLink.attributes}>Add card details</a></h4>
${selectOtherPaymentMethod()}
</div>
`;
@ -34,8 +30,8 @@ class CardView {
const cardIcons = {
'VISA': 'visa-light.svg',
'MASTER_CARD': 'mastercard-light.svg',
'AMEX': 'amex.svg',
'DISCOVER': 'discover.svg',
'AMEX': 'amex-light.svg',
'DISCOVER': 'discover-light.svg',
'DINERS': 'dinersclub-light.svg',
'JCB': 'jcb-light.svg',
'UNIONPAY': 'unionpay-light.svg',
@ -52,7 +48,7 @@ class CardView {
<img
class="ppcp-card-icon"
title="${data.value('brand')}"
src="${window.wc_ppcp_axo.module_url}/assets/images/axo/${cardIcons[data.value('brand')]}"
src="${window.wc_ppcp_axo.icons_directory}${cardIcons[data.value('brand')]}"
alt="${data.value('brand')}"
>
</div>

View file

@ -13,7 +13,10 @@ use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
@ -58,7 +61,8 @@ return array(
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.settings.status' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.url' )
);
},
@ -191,6 +195,40 @@ return array(
return '';
}
return '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>';
},
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ) {
$fastlane_settings_url = admin_url(
sprintf(
'admin.php?page=wc-settings&tab=checkout&section=%1$s&ppcp-tab=%2$s#field-axo_heading',
PayPalGateway::ID,
CreditCardGateway::ID
)
);
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. */
__(
'<span class="highlight">Important:</span> The <code>Cart</code> & <code>Classic Cart</code> <strong>Smart Button Locations</strong> cannot be disabled while <a href="%1$s">Fastlane</a> is active.',
'woocommerce-paypal-payments'
),
esc_url( $fastlane_settings_url )
);
} else {
return '';
}
return '<div class="ppcp-notice ppcp-notice-warning"><p>' . $notice_content . '</p></div>';
},
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint {
return new FrontendLoggerEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Assets;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Axo\FrontendLoggerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
@ -78,6 +79,13 @@ class AxoManager {
*/
private $session_handler;
/**
* The WcGateway module URL.
*
* @var string
*/
private $wcgateway_module_url;
/**
* AxoManager constructor.
*
@ -89,6 +97,7 @@ class AxoManager {
* @param SettingsStatus $settings_status The Settings status helper.
* @param string $currency 3-letter currency code of the shop.
* @param LoggerInterface $logger The logger.
* @param string $wcgateway_module_url The WcGateway module URL.
*/
public function __construct(
string $module_url,
@ -98,17 +107,19 @@ class AxoManager {
Environment $environment,
SettingsStatus $settings_status,
string $currency,
LoggerInterface $logger
LoggerInterface $logger,
string $wcgateway_module_url
) {
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
$this->wcgateway_module_url = $wcgateway_module_url;
}
/**
@ -151,13 +162,13 @@ class AxoManager {
*/
private function script_data() {
return array(
'environment' => array(
'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox',
),
'widgets' => array(
'widgets' => array(
'email' => 'render',
),
'insights' => array(
'insights' => array(
'enabled' => true,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' =>
@ -171,7 +182,7 @@ class AxoManager {
'value' => WC()->cart->get_total( 'numeric' ),
),
),
'style_options' => array(
'style_options' => array(
'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '',
'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
@ -190,14 +201,21 @@ class AxoManager {
'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '',
),
),
'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '',
'woocommerce' => array(
'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '',
'woocommerce' => array(
'states' => array(
'US' => WC()->countries->get_states( 'US' ),
'CA' => WC()->countries->get_states( 'CA' ),
),
),
'module_url' => untrailingslashit( $this->module_url ),
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array(
'frontend_logger' => array(
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
);
}

View file

@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
/**
* Class AxoModule
@ -43,7 +44,6 @@ class AxoModule implements ModuleInterface {
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
$module = $this;
add_filter(
'woocommerce_payment_gateways',
@ -108,9 +108,31 @@ class AxoModule implements ModuleInterface {
}
);
// Force 'cart-block' and 'cart' Smart Button locations in the settings.
add_action(
'admin_init',
static function () use ( $c ) {
$listener = $c->get( 'wcgateway.settings.listener' );
assert( $listener instanceof SettingsListener );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$listener->filter_settings(
$settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ),
'smart_button_locations',
function( array $existing_setting_value ) {
$axo_forced_locations = array( 'cart-block', 'cart' );
return array_unique( array_merge( $existing_setting_value, $axo_forced_locations ) );
}
);
}
);
add_action(
'init',
function () use ( $c, $module ) {
function () use ( $c ) {
$module = $this;
// Check if the module is applicable, correct country, currency, ... etc.
if ( ! $c->get( 'axo.eligible' ) ) {
@ -123,11 +145,15 @@ class AxoModule implements ModuleInterface {
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c, $manager ) {
static function () use ( $c, $manager, $module ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
if ( $smart_button->should_load_ppcp_script() ) {
if ( $module->should_render_fastlane( $settings ) && $smart_button->should_load_ppcp_script() ) {
$manager->enqueue();
}
}
@ -217,6 +243,18 @@ class AxoModule implements ModuleInterface {
1
);
add_action(
'wc_ajax_' . FrontendLoggerEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'axo.endpoint.frontend-logger' );
assert( $endpoint instanceof FrontendLoggerEndpoint );
$endpoint->handle_request();
}
);
// Add the markup necessary for displaying overlays and loaders for Axo on the checkout page.
$this->add_checkout_loader_markup( $c );
}
/**
@ -292,4 +330,47 @@ class AxoModule implements ModuleInterface {
&& CartCheckoutDetector::has_classic_checkout()
&& $is_axo_enabled;
}
/**
* Adds the markup necessary for displaying overlays and loaders for Axo on the checkout page.
*
* @param ContainerInterface $c The container.
* @return void
*/
private function add_checkout_loader_markup( ContainerInterface $c ): void {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( $this->should_render_fastlane( $settings ) ) {
add_action(
'woocommerce_checkout_before_customer_details',
function () {
echo '<div class="ppcp-axo-loading">';
}
);
add_action(
'woocommerce_checkout_after_customer_details',
function () {
echo '</div>';
}
);
add_action(
'woocommerce_checkout_billing',
function () {
echo '<div class="loader"><div class="ppcp-axo-overlay"></div>';
},
8
);
add_action(
'woocommerce_checkout_billing',
function () {
echo '</div>';
},
12
);
}
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* The endpoint to log entries from frontend.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class FrontendLoggerEndpoint
*/
class FrontendLoggerEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-frontend-logger';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* FrontendLoggerEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct( RequestData $request_data, LoggerInterface $logger ) {
$this->request_data = $request_data;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
$level = $data['log']['level'] ?? 'info';
switch ( $level ) {
case 'error':
$this->logger->error( '[AXO] ' . $data['log']['message'] );
break;
default:
$this->logger->info( '[AXO] ' . $data['log']['message'] );
break;
}
wp_send_json_success();
return true;
}
}

View file

@ -26,4 +26,16 @@ class PropertiesDictionary {
);
}
/**
* Returns the list of possible cardholder name options.
*
* @return array
*/
public static function cardholder_name_options(): array {
return array(
'yes' => __( 'Yes', 'woocommerce-paypal-payments' ),
'no' => __( 'No', 'woocommerce-paypal-payments' ),
);
}
}

View file

@ -51,16 +51,7 @@ return array(
);
}
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
if ( $subscription_helper->plugin_is_active() ) {
$label .= __(
'<div class="ppcp-notice ppcp-notice-warning"><p><span class="highlight">Important:</span> Cannot be deactivated while the WooCommerce Subscriptions plugin is active.</p></div>',
'woocommerce-paypal-payments'
);
}
$should_disable_checkbox = $subscription_helper->plugin_is_active() || apply_filters( 'woocommerce_paypal_payments_toggle_final_review_checkbox', false );
$should_disable_checkbox = apply_filters( 'woocommerce_paypal_payments_toggle_final_review_checkbox', false );
return $insert_after(
$fields,

View file

@ -23,6 +23,9 @@ import {
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap";
import {keysToCamelCase} from "../../../ppcp-button/resources/js/modules/Helper/Utils";
import {
handleShippingOptionsChange
} from "../../../ppcp-button/resources/js/modules/Helper/ShippingHandler";
const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource;
@ -146,7 +149,7 @@ const PayPalComponent = ({
shipping_address: addresses.shippingAddress,
}),
];
if (!config.finalReviewEnabled) {
if (shouldHandleShippingInPayPal()) {
// set address in UI
promises.push(wp.data.dispatch('wc/store/cart').setBillingAddress(addresses.billingAddress));
if (shippingData.needsShipping) {
@ -181,7 +184,7 @@ const PayPalComponent = ({
throw new Error(config.scriptData.labels.error.generic)
}
if (config.finalReviewEnabled) {
if (!shouldHandleShippingInPayPal()) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError(true);
@ -220,7 +223,7 @@ const PayPalComponent = ({
shipping_address: addresses.shippingAddress,
}),
];
if (!config.finalReviewEnabled) {
if (shouldHandleShippingInPayPal()) {
// set address in UI
promises.push(wp.data.dispatch('wc/store/cart').setBillingAddress(addresses.billingAddress));
if (shippingData.needsShipping) {
@ -255,7 +258,7 @@ const PayPalComponent = ({
throw new Error(config.scriptData.labels.error.generic)
}
if (config.finalReviewEnabled) {
if (!shouldHandleShippingInPayPal()) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError(true);
@ -297,8 +300,12 @@ const PayPalComponent = ({
onClick();
};
const isVenmoAndVaultingEnabled = () => {
return window.ppcpFundingSource === 'venmo' && config.scriptData.vaultingEnabled;
const shouldHandleShippingInPayPal = () => {
if (config.finalReviewEnabled) {
return false;
}
return window.ppcpFundingSource !== 'venmo' || !config.scriptData.vaultingEnabled;
}
let handleShippingOptionsChange = null;
@ -306,7 +313,7 @@ const PayPalComponent = ({
let handleSubscriptionShippingOptionsChange = null;
let handleSubscriptionShippingAddressChange = null;
if (shippingData.needsShipping && !config.finalReviewEnabled) {
if (shippingData.needsShipping && shouldHandleShippingInPayPal()) {
handleShippingOptionsChange = async (data, actions) => {
try {
const shippingOptionId = data.selectedShippingOption?.id;
@ -391,6 +398,21 @@ const PayPalComponent = ({
await shippingData.setShippingAddress(address);
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);
@ -447,7 +469,7 @@ const PayPalComponent = ({
if (config.scriptData.continuation) {
return true;
}
if (!config.finalReviewEnabled) {
if (shouldHandleShippingInPayPal()) {
location.href = getCheckoutRedirectUrl();
}
return true;
@ -493,8 +515,16 @@ const PayPalComponent = ({
onError={onClose}
createSubscription={createSubscription}
onApprove={handleApproveSubscription}
onShippingOptionsChange={handleSubscriptionShippingOptionsChange}
onShippingAddressChange={handleSubscriptionShippingAddressChange}
onShippingOptionsChange={(data, actions) => {
shouldHandleShippingInPayPal()
? handleSubscriptionShippingOptionsChange(data, actions)
: null;
}}
onShippingAddressChange={(data, actions) => {
shouldHandleShippingInPayPal()
? handleSubscriptionShippingAddressChange(data, actions)
: null;
}}
/>
);
}
@ -508,8 +538,16 @@ const PayPalComponent = ({
onError={onClose}
createOrder={createOrder}
onApprove={handleApprove}
onShippingOptionsChange={handleShippingOptionsChange}
onShippingAddressChange={handleShippingAddressChange}
onShippingOptionsChange={(data, actions) => {
shouldHandleShippingInPayPal()
? handleShippingOptionsChange(data, actions)
: null;
}}
onShippingAddressChange={(data, actions) => {
shouldHandleShippingInPayPal()
? handleShippingAddressChange(data, actions)
: null;
}}
/>
);
}

View file

@ -23,7 +23,8 @@ class CartActionHandler {
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
subscription_id: data.subscriptionID,
should_create_wc_order: !context.config.vaultingEnabled || data.paymentSource !== 'venmo'
})
}).then((res)=>{
return res.json();
@ -33,7 +34,9 @@ class CartActionHandler {
throw Error(data.data.message);
}
location.href = this.config.redirect;
let orderReceivedUrl = data.data?.order_received_url
location.href = orderReceivedUrl ? orderReceivedUrl : context.config.redirect;
});
},
onError: (err) => {
@ -60,8 +63,7 @@ class CartActionHandler {
funding_source: window.ppcpFundingSource,
bn_code:bnCode,
payer,
context:this.config.context,
payment_source: data.paymentSource
context:this.config.context
}),
}).then(function(res) {
return res.json();

View file

@ -4,6 +4,7 @@ import widgetBuilder from "../Renderer/WidgetBuilder";
import merge from "deepmerge";
import {keysToCamelCase} from "./Utils";
import {getCurrentPaymentMethod} from "./CheckoutMethodState";
import { v4 as uuidv4 } from 'uuid';
// This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
@ -63,9 +64,10 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
// Axo SDK options
const sdkClientToken = config?.axo?.sdk_client_token;
const uuid = uuidv4().replace(/-/g, '');
if(sdkClientToken) {
scriptOptions['data-sdk-client-token'] = sdkClientToken;
scriptOptions['data-client-metadata-id'] = 'ppcp-cm-id';
scriptOptions['data-client-metadata-id'] = uuid;
}
// Load PayPal script for special case with data-client-token

View file

@ -39,19 +39,21 @@ export const handleShippingOptionsChange = async (data, actions, config) => {
})
}
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
if (!config.data_client_id.has_subscriptions) {
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
if (!json.success) {
throw new Error(json.data.message);
}
}
} catch (e) {
console.error(e);
@ -104,20 +106,20 @@ export const handleShippingAddressChange = async (data, actions, config) => {
})
})
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);

View file

@ -68,14 +68,6 @@ class Renderer {
}
}
shouldHandleShippingInPaypal = (venmoButtonClicked) => {
if (!this.defaultSettings.should_handle_shipping_in_paypal) {
return false;
}
return !venmoButtonClicked || !this.defaultSettings.vaultingEnabled;
}
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) ) {
// Try to render registered buttons again in case they were removed from the DOM by an external source.
@ -93,7 +85,16 @@ class Renderer {
const options = {
style,
...contextConfig,
onClick: this.onSmartButtonClick,
onClick: (data, actions) => {
if (this.onSmartButtonClick) {
this.onSmartButtonClick(data, actions);
}
venmoButtonClicked = false;
if (data.fundingSource === 'venmo') {
venmoButtonClicked = true;
}
},
onInit: (data, actions) => {
if (this.onSmartButtonsInit) {
this.onSmartButtonsInit(data, actions);
@ -103,9 +104,17 @@ class Renderer {
};
// Check the condition and add the handler if needed
if (this.shouldHandleShippingInPaypal(venmoButtonClicked)) {
options.onShippingOptionsChange = (data, actions) => handleShippingOptionsChange(data, actions, this.defaultSettings);
options.onShippingAddressChange = (data, actions) => handleShippingAddressChange(data, actions, this.defaultSettings);
if (this.defaultSettings.should_handle_shipping_in_paypal) {
options.onShippingOptionsChange = (data, actions) => {
!this.isVenmoButtonClickedWhenVaultingIsEnabled(venmoButtonClicked)
? handleShippingOptionsChange(data, actions, this.defaultSettings)
: null;
}
options.onShippingAddressChange = (data, actions) => {
!this.isVenmoButtonClickedWhenVaultingIsEnabled(venmoButtonClicked)
? handleShippingAddressChange(data, actions, this.defaultSettings)
: null;
}
}
return options;
@ -139,6 +148,10 @@ class Renderer {
}
}
isVenmoButtonClickedWhenVaultingIsEnabled = (venmoButtonClicked) => {
return venmoButtonClicked && this.defaultSettings.vaultingEnabled;
}
isAlreadyRendered(wrapper, fundingSource) {
return this.renderedSources.has(wrapper + (fundingSource ?? ''));
}

View file

@ -239,7 +239,6 @@ return array(
$final_review_enabled = $container->get( 'blocks.settings.final_review_enabled' );
$wc_order_creator = $container->get( 'button.helper.wc-order-creator' );
$gateway = $container->get( 'wcgateway.paypal-gateway' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ApproveOrderEndpoint(
$request_data,
@ -252,7 +251,6 @@ return array(
$final_review_enabled,
$gateway,
$wc_order_creator,
$subscription_helper,
$logger
);
},
@ -260,7 +258,10 @@ return array(
return new ApproveSubscriptionEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' )
$container->get( 'session.handler' ),
$container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'button.helper.wc-order-creator' ),
$container->get( 'wcgateway.paypal-gateway' )
);
},
'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver {
@ -362,6 +363,10 @@ return array(
},
'button.helper.wc-order-creator' => static function ( ContainerInterface $container ): WooCommerceOrderCreator {
return new WooCommerceOrderCreator( $container->get( 'wcgateway.funding-source.renderer' ), $container->get( 'session.handler' ) );
return new WooCommerceOrderCreator(
$container->get( 'wcgateway.funding-source.renderer' ),
$container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' )
);
},
);

View file

@ -105,13 +105,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
*/
protected $wc_order_creator;
/**
* The Subscription Helper.
*
* @var SubscriptionHelper
*/
protected $subscription_helper;
/**
* The logger.
*
@ -132,7 +125,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
* @param bool $final_review_enabled Whether the final review is enabled.
* @param PayPalGateway $gateway The WC gateway.
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -146,7 +138,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
bool $final_review_enabled,
PayPalGateway $gateway,
WooCommerceOrderCreator $wc_order_creator,
SubscriptionHelper $subscription_helper,
LoggerInterface $logger
) {
@ -160,7 +151,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->final_review_enabled = $final_review_enabled;
$this->gateway = $gateway;
$this->wc_order_creator = $wc_order_creator;
$this->subscription_helper = $subscription_helper;
$this->logger = $logger;
}
@ -247,7 +237,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_order( $order );
if ( ! $this->subscription_helper->plugin_is_active() && apply_filters( 'woocommerce_paypal_payments_toggle_final_review_checkbox', false ) ) {
if ( apply_filters( 'woocommerce_paypal_payments_toggle_final_review_checkbox', false ) ) {
$this->toggle_final_review_enabled_setting();
}

View file

@ -11,13 +11,18 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class ApproveSubscriptionEndpoint
*/
class ApproveSubscriptionEndpoint implements EndpointInterface {
use ContextTrait;
const ENDPOINT = 'ppc-approve-subscription';
/**
@ -41,21 +46,51 @@ class ApproveSubscriptionEndpoint implements EndpointInterface {
*/
private $session_handler;
/**
* Whether the final review is enabled.
*
* @var bool
*/
protected $final_review_enabled;
/**
* The WooCommerce order creator.
*
* @var WooCommerceOrderCreator
*/
protected $wc_order_creator;
/**
* The WC gateway.
*
* @var PayPalGateway
*/
protected $gateway;
/**
* ApproveSubscriptionEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param bool $final_review_enabled Whether the final review is enabled.
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
* @param PayPalGateway $gateway The WC gateway.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler
SessionHandler $session_handler,
bool $final_review_enabled,
WooCommerceOrderCreator $wc_order_creator,
PayPalGateway $gateway
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->final_review_enabled = $final_review_enabled;
$this->wc_order_creator = $wc_order_creator;
$this->gateway = $gateway;
}
/**
@ -88,6 +123,15 @@ class ApproveSubscriptionEndpoint implements EndpointInterface {
WC()->session->set( 'ppcp_subscription_id', $data['subscription_id'] );
}
$should_create_wc_order = $data['should_create_wc_order'] ?? false;
if ( ! $this->final_review_enabled && ! $this->is_checkout() && $should_create_wc_order ) {
$wc_order = $this->wc_order_creator->create_from_paypal_order( $order, WC()->cart );
$this->gateway->process_payment( $wc_order->get_id() );
$order_received_url = $wc_order->get_checkout_order_received_url();
wp_send_json_success( array( 'order_received_url' => $order_received_url ) );
}
wp_send_json_success();
return true;
}

View file

@ -246,7 +246,6 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->parsed_request_data = $data;
$payment_method = $data['payment_method'] ?? '';
$funding_source = $data['funding_source'] ?? '';
$payment_source = $data['payment_source'] ?? '';
$wc_order = null;
if ( 'pay-now' === $data['context'] ) {
$wc_order = wc_get_order( (int) $data['order_id'] );
@ -262,7 +261,7 @@ class CreateOrderEndpoint implements EndpointInterface {
}
$this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
} else {
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->should_handle_shipping_in_paypal( $payment_source ) );
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->should_handle_shipping_in_paypal( $funding_source ) );
// Do not allow completion by webhooks when started via non-checkout buttons,
// it is needed only for some APMs in checkout.
@ -615,16 +614,16 @@ class CreateOrderEndpoint implements EndpointInterface {
/**
* Checks if the shipping should be handled in PayPal popup.
*
* @param string $payment_source The payment source.
* @param string $funding_source The funding source.
* @return bool true if the shipping should be handled in PayPal popup, otherwise false.
*/
protected function should_handle_shipping_in_paypal( string $payment_source ): bool {
protected function should_handle_shipping_in_paypal( string $funding_source ): bool {
$is_vaulting_enabled = $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' );
if ( ! $this->handle_shipping_in_paypal ) {
return false;
}
return ! $is_vaulting_enabled || $payment_source !== 'venmo';
return ! $is_vaulting_enabled || $funding_source !== 'venmo';
}
}

View file

@ -14,12 +14,16 @@ use WC_Cart;
use WC_Order;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
use WC_Subscription;
use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WP_Error;
/**
* Class WooCommerceOrderCreator
@ -40,18 +44,28 @@ class WooCommerceOrderCreator {
*/
protected $session_handler;
/**
* The subscription helper
*
* @var SubscriptionHelper
*/
protected $subscription_helper;
/**
* WooCommerceOrderCreator constructor.
*
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param SessionHandler $session_handler The session handler.
* @param SubscriptionHelper $subscription_helper The subscription helper.
*/
public function __construct(
FundingSourceRenderer $funding_source_renderer,
SessionHandler $session_handler
SessionHandler $session_handler,
SubscriptionHelper $subscription_helper
) {
$this->funding_source_renderer = $funding_source_renderer;
$this->session_handler = $session_handler;
$this->subscription_helper = $subscription_helper;
}
/**
@ -69,10 +83,13 @@ class WooCommerceOrderCreator {
throw new RuntimeException( 'Problem creating WC order.' );
}
$this->configure_line_items( $wc_order, $wc_cart );
$this->configure_shipping( $wc_order, $order->payer(), $order->purchase_units()[0]->shipping() );
$payer = $order->payer();
$shipping = $order->purchase_units()[0]->shipping();
$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
$this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping );
$this->configure_shipping( $wc_order, $payer, $shipping );
$this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() );
$wc_order->calculate_totals();
@ -84,11 +101,13 @@ class WooCommerceOrderCreator {
/**
* Configures the line items.
*
* @param WC_Order $wc_order The WC order.
* @param WC_Cart $wc_cart The Cart.
* @param WC_Order $wc_order The WC order.
* @param WC_Cart $wc_cart The Cart.
* @param Payer|null $payer The payer.
* @param Shipping|null $shipping The shipping.
* @return void
*/
protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart ): void {
protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart, ?Payer $payer, ?Shipping $shipping ): void {
$cart_contents = $wc_cart->get_cart();
foreach ( $cart_contents as $cart_item ) {
@ -111,9 +130,37 @@ class WooCommerceOrderCreator {
return;
}
$total = $product->get_price() * $quantity;
$item->set_name( $product->get_name() );
$item->set_subtotal( $product->get_price() * $quantity );
$item->set_total( $product->get_price() * $quantity );
$item->set_subtotal( $total );
$item->set_total( $total );
$product_id = $product->get_id();
if ( $this->is_subscription( $product_id ) ) {
$subscription = $this->create_subscription( $wc_order, $product_id );
$sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
$subscription_total = $total + $sign_up_fee;
$item->set_subtotal( $subscription_total );
$item->set_total( $subscription_total );
$subscription->add_product( $product );
$this->configure_shipping( $subscription, $payer, $shipping );
$this->configure_payment_source( $subscription );
$this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() );
$dates = array(
'trial_end' => WC_Subscriptions_Product::get_trial_expiration_date( $product_id ),
'next_payment' => WC_Subscriptions_Product::get_first_renewal_payment_date( $product_id ),
'end' => WC_Subscriptions_Product::get_expiration_date( $product_id ),
);
$subscription->update_dates( $dates );
$subscription->calculate_totals();
$subscription->payment_complete_for_order( $wc_order );
}
$wc_order->add_item( $item );
}
@ -179,8 +226,18 @@ class WooCommerceOrderCreator {
$shipping->set_method_id( $shipping_options->id() );
$shipping->set_total( $shipping_options->amount()->value_str() );
$items = $wc_order->get_items();
$items_in_package = array();
foreach ( $items as $item ) {
$items_in_package[] = $item->get_name() . ' &times; ' . (string) $item->get_quantity();
}
$shipping->add_meta_data( __( 'Items', 'woocommerce-paypal-payments' ), implode( ', ', $items_in_package ) );
$wc_order->add_item( $shipping );
}
$wc_order->calculate_totals();
}
/**
@ -225,4 +282,43 @@ class WooCommerceOrderCreator {
}
}
/**
* Checks if the product with given ID is WC subscription.
*
* @param int $product_id The product ID.
* @return bool true if the product is subscription, otherwise false.
*/
protected function is_subscription( int $product_id ): bool {
if ( ! $this->subscription_helper->plugin_is_active() ) {
return false;
}
return WC_Subscriptions_Product::is_subscription( $product_id );
}
/**
* Creates WC subscription from given order and product ID.
*
* @param WC_Order $wc_order The WC order.
* @param int $product_id The product ID.
* @return WC_Subscription The subscription order
* @throws RuntimeException If problem creating.
*/
protected function create_subscription( WC_Order $wc_order, int $product_id ): WC_Subscription {
$subscription = wcs_create_subscription(
array(
'order_id' => $wc_order->get_id(),
'status' => 'pending',
'billing_period' => WC_Subscriptions_Product::get_period( $product_id ),
'billing_interval' => WC_Subscriptions_Product::get_interval( $product_id ),
'customer_id' => $wc_order->get_customer_id(),
)
);
if ( $subscription instanceof WP_Error ) {
throw new RuntimeException( $subscription->get_error_message() );
}
return $subscription;
}
}

View file

@ -61,17 +61,33 @@ $background-ident-color: #fbfbfb;
border: 1px solid #c3c4c7;
border-left-width: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
margin: 5px 0px 2px;
padding: 0px 12px 4px 12px;
margin: 5px 15px 2px 0;
padding: 1px 12px;
p, .form-table td & p {
margin-top: 4px;
margin-bottom: 4px;
}
.highlight {
background: transparent;
font-weight: 600;
}
}
.ppcp-notice-warning {
border-left-color: #dba617;
.highlight {
background: transparent;
color: #dba617;
font-weight: 600;
}
}
.ppcp-notice-error {
border-left-color: #d63638;
.highlight {
color: #d63638;
}
}

View file

@ -360,7 +360,6 @@ return array(
$container->get( 'api.partner_merchant_id-production' ),
$container->get( 'api.partner_merchant_id-sandbox' ),
$container->get( 'api.endpoint.billing-agreements' ),
$container->get( 'wc-subscriptions.helper' ),
$logger
);
},

View file

@ -114,7 +114,7 @@ class CartCheckoutDetector {
*/
public static function has_classic_checkout(): bool {
$checkout_page_id = wc_get_page_id( 'checkout' );
return $checkout_page_id && has_block( 'woocommerce/classic-shortcode', $checkout_page_id );
return $checkout_page_id && ( has_block( 'woocommerce/classic-shortcode', $checkout_page_id ) || self::has_classic_shortcode( $checkout_page_id, 'woocommerce_checkout' ) );
}
/**
@ -124,6 +124,25 @@ class CartCheckoutDetector {
*/
public static function has_classic_cart(): bool {
$cart_page_id = wc_get_page_id( 'cart' );
return $cart_page_id && has_block( 'woocommerce/classic-shortcode', $cart_page_id );
return $cart_page_id && ( has_block( 'woocommerce/classic-shortcode', $cart_page_id ) || self::has_classic_shortcode( $cart_page_id, 'woocommerce_cart' ) );
}
/**
* Check if a page has a specific shortcode.
*
* @param int $page_id The ID of the page.
* @param string $shortcode The shortcode to check for.
*
* @return bool
*/
private static function has_classic_shortcode( int $page_id, string $shortcode ): bool {
if ( ! $page_id ) {
return false;
}
$page = get_post( $page_id );
$page_content = is_object( $page ) ? $page->post_content : '';
return str_contains( $page_content, $shortcode );
}
}

View file

@ -56,14 +56,14 @@ class PayUponInvoiceHelper {
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$billing_country = wc_clean( wp_unslash( $_POST['country'] ?? '' ) );
if ( $billing_country && 'DE' !== $billing_country ) {
$billing_country = WC()->customer->get_billing_country();
if ( empty( $billing_country ) || 'DE' !== $billing_country ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$shipping_country = wc_clean( wp_unslash( $_POST['s_country'] ?? '' ) );
if ( $shipping_country && 'DE' !== $shipping_country ) {
$shipping_country = WC()->customer->get_shipping_country();
if ( empty( $shipping_country ) || 'DE' !== $shipping_country ) {
return false;
}

View file

@ -220,14 +220,9 @@ class OrderProcessor {
);
throw new PayPalOrderMissingException(
sprintf(
// translators: %s: Order history URL on My Account section.
esc_attr__(
'There was an error processing your order. Please check for any charges in your payment method and review your <a href="%s">order history</a> before placing the order again.',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Intentionally "woocommerce" to reflect the original message.
'woocommerce'
),
esc_url( wc_get_account_endpoint_url( 'orders' ) )
esc_attr__(
'There was an error processing your order. Please check for any charges in your payment method and review your order history before placing the order again.',
'woocommerce-paypal-payments'
)
);
}

View file

@ -36,6 +36,8 @@ return function ( ContainerInterface $container, array $fields ): array {
</div>';
};
$axo_smart_button_location_notice = $container->has( 'axo.smart-button-location-notice' ) ? $container->get( 'axo.smart-button-location-notice' ) : '';
$smart_button_fields = array(
'button_style_heading' => array(
'heading' => __( 'PayPal Smart Buttons', 'woocommerce-paypal-payments' ),
@ -65,7 +67,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'type' => 'ppcp-multiselect',
'input_class' => array( 'wc-enhanced-select' ),
'default' => $container->get( 'wcgateway.button.default-locations' ),
'description' => __( 'Select where the PayPal smart buttons should be displayed.', 'woocommerce-paypal-payments' ),
'description' => __( 'Select where the PayPal smart buttons should be displayed.', 'woocommerce-paypal-payments' ) . $axo_smart_button_location_notice,
'options' => $container->get( 'wcgateway.button.locations' ),
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),

View file

@ -10,8 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
@ -23,9 +21,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
/**
@ -161,13 +157,6 @@ class SettingsListener {
*/
private $billing_agreements_endpoint;
/**
* The subscription helper
*
* @var SubscriptionHelper
*/
protected $subscription_helper;
/**
* The logger.
*
@ -193,7 +182,6 @@ class SettingsListener {
* @param string $partner_merchant_id_production Partner merchant ID production.
* @param string $partner_merchant_id_sandbox Partner merchant ID sandbox.
* @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param ?LoggerInterface $logger The logger.
*/
public function __construct(
@ -212,7 +200,6 @@ class SettingsListener {
string $partner_merchant_id_production,
string $partner_merchant_id_sandbox,
BillingAgreementsEndpoint $billing_agreements_endpoint,
SubscriptionHelper $subscription_helper,
LoggerInterface $logger = null
) {
@ -231,7 +218,6 @@ class SettingsListener {
$this->partner_merchant_id_production = $partner_merchant_id_production;
$this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox;
$this->billing_agreements_endpoint = $billing_agreements_endpoint;
$this->subscription_helper = $subscription_helper;
$this->logger = $logger ?: new NullLogger();
}
@ -394,7 +380,18 @@ class SettingsListener {
if ( $reference_transaction_enabled !== true ) {
$this->settings->set( 'vault_enabled', false );
$this->settings->set( 'subscriptions_mode', 'subscriptions_api' );
/**
* If Vaulting-API was previously enabled, then fall-back to the
* PayPal subscription mode, to ensure subscriptions are still
* possible on this shop.
*
* This can happen when switching to a different PayPal merchant account
*/
if ( 'vaulting_api' === $subscription_mode ) {
$this->settings->set( 'subscriptions_mode', 'subscriptions_api' );
}
$this->settings->persist();
}
@ -403,11 +400,6 @@ class SettingsListener {
$this->settings->persist();
}
if ( $this->subscription_helper->plugin_is_active() ) {
$this->settings->set( 'blocks_final_review_enabled', true );
$this->settings->persist();
}
if ( $subscription_mode === 'disable_paypal_subscriptions' && $vault_enabled === '1' ) {
$this->settings->set( 'vault_enabled', false );
$this->settings->persist();
@ -745,4 +737,28 @@ class SettingsListener {
}
}
/**
* Filter settings based on a condition.
*
* @param bool $condition The condition.
* @param string $setting_slug The setting slug.
* @param callable $filter_function The filter function.
* @param bool $persist Whether to persist the settings.
*/
public function filter_settings( bool $condition, string $setting_slug, callable $filter_function, bool $persist = true ): void {
if ( ! $this->is_valid_site_request() || State::STATE_ONBOARDED !== $this->state->current_state() ) {
return;
}
$existing_setting_value = $this->settings->has( $setting_slug ) ? $this->settings->get( $setting_slug ) : null;
if ( $condition ) {
$new_setting_value = $filter_function( $existing_setting_value );
$this->settings->set( $setting_slug, $new_setting_value );
if ( $persist ) {
$this->settings->persist();
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
"version": "2.7.1",
"version": "2.8.0",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",
@ -94,6 +94,7 @@
"dotenv": "^16.0.3",
"npm-run-all": "^4.1.5",
"playwright": "^1.43.0",
"run-s": "^0.0.0"
"run-s": "^0.0.0",
"uuid": "^9.0.1"
}
}

View file

@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, checkout, cart, pay later, apple
Requires at least: 5.3
Tested up to: 6.5
Requires PHP: 7.2
Stable tag: 2.7.1
Stable tag: 2.8.0
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -179,6 +179,19 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
= 2.8.0 - xxxx-xx-xx =
* Fix - Calculate totals after adding shipping to include taxes #2296
* Fix - Package tracking integration throws error in 2.7.1 #2289
* Fix - Make PayPal Subscription products unique in cart #2265
* Fix - PayPal declares subscription support when merchant not enabled for Reference Transactions #2282
* Fix - Google Pay and Apple Pay Settings button from Connection tab have wrong links #2273
* Fix - Smart Buttons in Block Checkout not respecting the location setting (2830) #2278
* Fix - Disable Pay Upon Invoice if billing/shipping country not set #2281
* Enhancement - Enable shipping callback for WC subscriptions #2259
* Enhancement - Disable the shipping callback for "venmo" when vaulting is active #2269
* Enhancement - Improve "Could not retrieve order" error message #2271
* Enhancement - Add block Checkout compatibility to Advanced Card Processing #2246
= 2.7.1 - 2024-05-28 =
* Fix - Ensure package tracking data is sent to original PayPal transaction #2180
* Fix - Set the 'Woo_PPCP' as a default value for data-partner-attribution-id #2188

View file

@ -60,7 +60,6 @@ class SettingsListenerTest extends ModularTestCase
'',
'',
$billing_agreement_endpoint,
$subscription_helper,
$logger
);

View file

@ -3,14 +3,14 @@
* Plugin Name: WooCommerce PayPal Payments
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
* Version: 2.7.1
* Version: 2.8.0
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
* Requires PHP: 7.2
* Requires Plugins: woocommerce
* WC requires at least: 3.9
* WC tested up to: 8.8
* WC tested up to: 8.9
* Text Domain: woocommerce-paypal-payments
*
* @package WooCommerce\PayPalCommerce
@ -26,7 +26,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
define( 'PAYPAL_URL', 'https://www.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' );
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-05-13' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-06-03' );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );
! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) && define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' );

View file

@ -598,6 +598,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"