diff --git a/changelog.txt b/changelog.txt index 3ca38e0e2..17c03cb83 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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 diff --git a/modules/ppcp-api-client/src/Authentication/SdkClientToken.php b/modules/ppcp-api-client/src/Authentication/SdkClientToken.php index 200ba2322..ba0f3a373 100644 --- a/modules/ppcp-api-client/src/Authentication/SdkClientToken.php +++ b/modules/ppcp-api-client/src/Authentication/SdkClientToken.php @@ -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; diff --git a/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php b/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php index 08af8c79e..3c21d4161 100644 --- a/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php +++ b/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php @@ -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() diff --git a/modules/ppcp-axo/extensions.php b/modules/ppcp-axo/extensions.php index e6d0f4836..8d8bf4378 100644 --- a/modules/ppcp-axo/extensions.php +++ b/modules/ppcp-axo/extensions.php @@ -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. -

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.

', - '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' ), '', @@ -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', diff --git a/modules/ppcp-axo/resources/css/styles.scss b/modules/ppcp-axo/resources/css/styles.scss index 605ac18b2..3513d0642 100644 --- a/modules/ppcp-axo/resources/css/styles.scss +++ b/modules/ppcp-axo/resources/css/styles.scss @@ -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; diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index a250c09b7..960da7f26 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -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 : '')); + log(`Email changed: ${this.emailInput ? this.emailInput.value : ''}`); 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; diff --git a/modules/ppcp-axo/resources/js/Components/DomElementCollection.js b/modules/ppcp-axo/resources/js/Components/DomElementCollection.js index fc44a2823..35f19ec6d 100644 --- a/modules/ppcp-axo/resources/js/Components/DomElementCollection.js +++ b/modules/ppcp-axo/resources/js/Components/DomElementCollection.js @@ -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({ diff --git a/modules/ppcp-axo/resources/js/Helper/Debug.js b/modules/ppcp-axo/resources/js/Helper/Debug.js index e473d4a3b..c08fa8087 100644 --- a/modules/ppcp-axo/resources/js/Helper/Debug.js +++ b/modules/ppcp-axo/resources/js/Helper/Debug.js @@ -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}`); + } + }); } diff --git a/modules/ppcp-axo/resources/js/Views/BillingView.js b/modules/ppcp-axo/resources/js/Views/BillingView.js index aba44afcf..f2903d4ef 100644 --- a/modules/ppcp-axo/resources/js/Views/BillingView.js +++ b/modules/ppcp-axo/resources/js/Views/BillingView.js @@ -34,22 +34,7 @@ class BillingView { `; } - return ` -
-
-

Billing

-
Edit -
-
${data.value('email')}
-
${data.value('company')}
-
${data.value('firstName')} ${data.value('lastName')}
-
${data.value('street1')}
-
${data.value('street2')}
-
${data.value('postCode')} ${data.value('city')}
-
${valueOfSelect('#billing_state', data.value('stateCode'))}
-
${valueOfSelect('#billing_country', data.value('countryCode'))}
-
- `; + return ''; }, fields: { email: { diff --git a/modules/ppcp-axo/resources/js/Views/CardView.js b/modules/ppcp-axo/resources/js/Views/CardView.js index 9ca8670a2..82c15079c 100644 --- a/modules/ppcp-axo/resources/js/Views/CardView.js +++ b/modules/ppcp-axo/resources/js/Views/CardView.js @@ -20,10 +20,6 @@ class CardView { if (data.isEmpty()) { return `
-
-
Please fill in your card details.
-
-

Add card details

${selectOtherPaymentMethod()}
`; @@ -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 { ${data.value('brand')} diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 44253f22d..0020169a7 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -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 '

' . $notice_content . '

'; + }, + + '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§ion=%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. */ + __( + 'Important: The Cart & Classic Cart Smart Button Locations cannot be disabled while Fastlane is active.', + 'woocommerce-paypal-payments' + ), + esc_url( $fastlane_settings_url ) + ); + } else { + return ''; + } + return '

' . $notice_content . '

'; }, + 'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint { + return new FrontendLoggerEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, ); diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php index 2ed5fc04c..4e75f56fa 100644 --- a/modules/ppcp-axo/src/Assets/AxoManager.php +++ b/modules/ppcp-axo/src/Assets/AxoManager.php @@ -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() ), + ), + ), ); } diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index dfb76ef60..d1731b4fe 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -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 '
'; + } + ); + + add_action( + 'woocommerce_checkout_after_customer_details', + function () { + echo '
'; + } + ); + + add_action( + 'woocommerce_checkout_billing', + function () { + echo '
'; + }, + 8 + ); + + add_action( + 'woocommerce_checkout_billing', + function () { + echo '
'; + }, + 12 + ); + } + } } diff --git a/modules/ppcp-axo/src/FrontendLoggerEndpoint.php b/modules/ppcp-axo/src/FrontendLoggerEndpoint.php new file mode 100644 index 000000000..e17a819e4 --- /dev/null +++ b/modules/ppcp-axo/src/FrontendLoggerEndpoint.php @@ -0,0 +1,80 @@ +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; + } +} diff --git a/modules/ppcp-axo/src/Helper/PropertiesDictionary.php b/modules/ppcp-axo/src/Helper/PropertiesDictionary.php index 024c3649c..a07c4ace3 100644 --- a/modules/ppcp-axo/src/Helper/PropertiesDictionary.php +++ b/modules/ppcp-axo/src/Helper/PropertiesDictionary.php @@ -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' ), + ); + } + } diff --git a/modules/ppcp-blocks/extensions.php b/modules/ppcp-blocks/extensions.php index c2b607780..b32f5bbc6 100644 --- a/modules/ppcp-blocks/extensions.php +++ b/modules/ppcp-blocks/extensions.php @@ -51,16 +51,7 @@ return array( ); } - $subscription_helper = $container->get( 'wc-subscriptions.helper' ); - - if ( $subscription_helper->plugin_is_active() ) { - $label .= __( - '

Important: Cannot be deactivated while the WooCommerce Subscriptions plugin is active.

', - '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, diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 240a0d709..10d035714 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -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; + }} /> ); } diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js index 2b3066395..ce580fbb8 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js @@ -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(); diff --git a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js index dce3136c8..55866fe9d 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js @@ -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 diff --git a/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js b/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js index 49f45ab2d..c67bee5bb 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js @@ -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); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 895a7845b..a95621456 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -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 ?? '')); } diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 3d14c9a74..aa518f13f 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -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' ) + ); }, ); diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index 9e3d43c6f..775be302e 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -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(); } diff --git a/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php index d2ca73b83..d89a0a1af 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php @@ -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; } diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index fbfb22309..868f9bf6c 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -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'; } } diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index 92f07c0fd..625157a05 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -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() . ' × ' . (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; + } } diff --git a/modules/ppcp-wc-gateway/resources/css/common.scss b/modules/ppcp-wc-gateway/resources/css/common.scss index 997b72e2d..edc9b4689 100644 --- a/modules/ppcp-wc-gateway/resources/css/common.scss +++ b/modules/ppcp-wc-gateway/resources/css/common.scss @@ -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; } } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index da7ba6908..412b1d3b2 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -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 ); }, diff --git a/modules/ppcp-wc-gateway/src/Helper/CartCheckoutDetector.php b/modules/ppcp-wc-gateway/src/Helper/CartCheckoutDetector.php index faac7f257..e18fb703e 100644 --- a/modules/ppcp-wc-gateway/src/Helper/CartCheckoutDetector.php +++ b/modules/ppcp-wc-gateway/src/Helper/CartCheckoutDetector.php @@ -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 ); } } diff --git a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php index 823caf611..b91eca8a0 100644 --- a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php +++ b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php @@ -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; } diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php index 4d1363361..82183e0d4 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php @@ -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 order history 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' ) ); } diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php index 0d02ad679..460ff3592 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php @@ -36,6 +36,8 @@ return function ( ContainerInterface $container, array $fields ): array { '; }; + $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(), diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index 6966f97c0..9986d5edc 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -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(); + } + } + } } diff --git a/package.json b/package.json index bf52a8677..d66c8e898 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/readme.txt b/readme.txt index 0c6bdbb1a..ac1315805 100644 --- a/readme.txt +++ b/readme.txt @@ -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 diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index 92d3f50d1..c34146f3e 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -60,7 +60,6 @@ class SettingsListenerTest extends ModularTestCase '', '', $billing_agreement_endpoint, - $subscription_helper, $logger ); diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index ce5883709..62d4359aa 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -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_' ); diff --git a/yarn.lock b/yarn.lock index 722df19c5..81af54726 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"