diff --git a/changelog.txt b/changelog.txt index a840377a9..52e9534b1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,25 @@ *** Changelog *** += 2.4.1 - xxxx-xx-xx = +* Fix - Error "PayPal order ID not found in meta" prevents automations from triggering when buying subscription via third-party payment gateway #1822 +* Fix - Card button subscription support declaration #1796 +* Fix - Pay Later messaging disappears when updating shipping option on cart page #1807 +* Fix - Apple Pay payment from single product may fail after changing shipping options in Apple Pay payment sheet #1810 +* Enhancement - Extend list of supported countries for Advanced Card Processing #1808 +* Enhancement - Extend Apple Pay/Google Pay country eligibility to Italy #1811 +* Enhancement - Override language used to display PayPal buttons #600 +* Enhancement - Apple Pay button preview #1824 +* Enhancement - Add Apple Pay & Google Pay logos on the onboarding page #1823 +* Enhancement - Improve Apple Pay compatibility with variable products on single product page #1803 +* Enhancement - Apple Pay domain registration & browser eligibility check #1821 +* Enhancement - Package Tracking compatibility with WooCommerce Shipping & ShipStation for WooCommerce #1813 +* Enhancement - Fill form when continuation in block #1794 +* Enhancement - Display Shop location Pay Later messaging on product category pages #1809 +* Enhancement - Present apple-developer-merchantid-domain-association file only when Apple Pay is enabled #1818 +* Enhancement - Improve Apple Pay compatibility on Pay for Order page #1815 +* Enhancement - Display Pay Later messages before the payment methods on the Pay for Order page #1814 +* Enhancement - Handle undefined array key warnings on PHP 8.1 #1804 + = 2.4.0 - 2023-10-31 = * Fix - Mini-Cart Bug cause of wrong DOM-Structure in v2.3.1 #1735 * Fix - ACDC disappearing after plugin updates #1751 diff --git a/modules/ppcp-applepay/extensions.php b/modules/ppcp-applepay/extensions.php index 39aa3ed81..bdea5a19f 100644 --- a/modules/ppcp-applepay/extensions.php +++ b/modules/ppcp-applepay/extensions.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Applepay; use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager; @@ -37,6 +38,34 @@ return array( $display_manager = $container->get( 'wcgateway.display-manager' ); assert( $display_manager instanceof DisplayManager ); + // Domain registration. + $env = $container->get( 'onboarding.environment' ); + assert( $env instanceof Environment ); + + $domain_registration_url = 'https://www.paypal.com/uccservicing/apm/applepay'; + if ( $env->current_environment_is( Environment::SANDBOX ) ) { + $domain_registration_url = 'https://www.sandbox.paypal.com/uccservicing/apm/applepay'; + } + + // Domain validation. + $domain_validation_text = __( 'Status: Domain validation failed ❌', 'woocommerce-paypal-payments' ); + if ( $container->get( 'applepay.is_validated' ) ) { + $domain_validation_text = __( 'Status: Domain successfully validated ✔️', 'woocommerce-paypal-payments' ); + } + + // Device eligibility. + $device_eligibility_text = __( 'Your current browser/device does not seem to support Apple Pay ❌.', 'woocommerce-paypal-payments' ); + $device_eligibility_notes = sprintf( + // translators: %1$s and %2$s are the opening and closing of HTML tag. + __( 'Though the button may display in previews, it won\'t appear in the shop. For details, refer to the %1$sApple Pay requirements%2$s.', 'woocommerce-paypal-payments' ), + '', + '' + ); + if ( $container->get( 'applepay.is_browser_supported' ) ) { + $device_eligibility_text = __( 'Your browser/device supports Apple Pay ✔️.', 'woocommerce-paypal-payments' ); + $device_eligibility_notes = __( 'The Apple Pay button will be visible both in previews and below the PayPal buttons in the shop.', 'woocommerce-paypal-payments' ); + } + // Connection tab fields. $fields = $insert_after( $fields, @@ -100,7 +129,7 @@ return array( $fields, 'allow_card_button_gateway', array( - 'applepay_button_enabled' => array( + 'applepay_button_enabled' => array( 'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ), 'type' => 'checkbox', 'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' ) @@ -122,6 +151,9 @@ return array( $display_manager ->rule() ->condition_element( 'applepay_button_enabled', '1' ) + ->action_visible( 'applepay_button_domain_registration' ) + ->action_visible( 'applepay_button_domain_validation' ) + ->action_visible( 'applepay_button_device_eligibility' ) ->action_visible( 'applepay_button_color' ) ->action_visible( 'applepay_button_type' ) ->action_visible( 'applepay_button_language' ) @@ -130,7 +162,66 @@ return array( ), ), ), - 'applepay_button_type' => array( + 'applepay_button_domain_registration' => array( + 'title' => str_repeat( ' ', 6 ) . __( 'Domain Registration', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-text', + 'text' => + '' + . __( 'Manage Domain Registration', 'woocommerce-paypal-payments' ) + . '' + . '

' + . __( 'Any (sub)domain names showing an Apple Pay button must be registered on the PayPal website. If the domain displaying the Apple Pay button isn\'t registered, the payment method won\'t work.', 'woocommerce-paypal-payments' ) + . '

', + 'desc_tip' => true, + 'description' => __( + 'Registering the website domain on the PayPal site is mandated by Apple. Payments will fail if the Apple Pay button is used on an unregistered domain.', + 'woocommerce-paypal-payments' + ), + 'class' => array(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'paypal', + 'requirements' => array(), + ), + 'applepay_button_domain_validation' => array( + 'title' => str_repeat( ' ', 6 ) . __( 'Domain Validation', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-text', + 'text' => $domain_validation_text + . '

' + . sprintf( + // translators: %1$s and %2$s are the opening and closing of HTML tag. + __( 'Note: PayPal Payments automatically presents the %1$sdomain association file%2$s for Apple to validate your registered domain.', 'woocommerce-paypal-payments' ), + '', + '' + ) + . '

', + 'desc_tip' => true, + 'description' => __( + 'Apple requires the website domain to be registered and validated. PayPal Payments automatically presents your domain association file for Apple to validate the manually registered domain.', + 'woocommerce-paypal-payments' + ), + 'class' => array(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'paypal', + 'requirements' => array(), + ), + 'applepay_button_device_eligibility' => array( + 'title' => str_repeat( ' ', 6 ) . __( 'Device Eligibility', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-text', + 'text' => $device_eligibility_text + . '

' + . $device_eligibility_notes + . '

', + 'desc_tip' => true, + 'description' => __( + 'Apple Pay demands certain Apple devices for secure payment execution. This helps determine if your current device is compliant.', + 'woocommerce-paypal-payments' + ), + 'class' => array(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'paypal', + 'requirements' => array(), + ), + 'applepay_button_type' => array( 'title' => str_repeat( ' ', 6 ) . __( 'Button Label', 'woocommerce-paypal-payments' ), 'type' => 'select', 'desc_tip' => true, @@ -146,7 +237,7 @@ return array( 'gateway' => 'paypal', 'requirements' => array(), ), - 'applepay_button_color' => array( + 'applepay_button_color' => array( 'title' => str_repeat( ' ', 6 ) . __( 'Button Color', 'woocommerce-paypal-payments' ), 'type' => 'select', 'desc_tip' => true, @@ -163,7 +254,7 @@ return array( 'gateway' => 'paypal', 'requirements' => array(), ), - 'applepay_button_language' => array( + 'applepay_button_language' => array( 'title' => str_repeat( ' ', 6 ) . __( 'Button Language', 'woocommerce-paypal-payments' ), 'type' => 'select', 'desc_tip' => true, diff --git a/modules/ppcp-applepay/resources/css/styles.scss b/modules/ppcp-applepay/resources/css/styles.scss index d3f834119..5eabe5640 100644 --- a/modules/ppcp-applepay/resources/css/styles.scss +++ b/modules/ppcp-applepay/resources/css/styles.scss @@ -1,11 +1,11 @@ -#applepay-container { +#applepay-container, .ppcp-button-applepay { --apple-pay-button-height: 45px; --apple-pay-button-min-height: 40px; --apple-pay-button-width: 100%; --apple-pay-button-max-width: 750px; --apple-pay-button-border-radius: 4px; --apple-pay-button-overflow: hidden; - --apple-pay-button-margin:7px 0; + margin:7px 0; &.ppcp-button-pill { --apple-pay-button-border-radius: 50px; } @@ -17,7 +17,7 @@ } .woocommerce-checkout { - #applepay-container { + #applepay-container, .ppcp-button-applepay { margin-top: 0.5em; --apple-pay-button-border-radius: 4px; --apple-pay-button-height: 45px; @@ -30,7 +30,7 @@ .ppcp-has-applepay-block { .wp-block-woocommerce-checkout { - #applepay-container { + #applepay-container, .ppcp-button-applepay { --apple-pay-button-margin: 0; --apple-pay-button-height: 40px; &.ppcp-button-pill { @@ -40,7 +40,7 @@ } .wp-block-woocommerce-cart { - #applepay-container { + #applepay-container, .ppcp-button-applepay { --apple-pay-button-margin: 0; --apple-pay-button-height: 40px; } @@ -52,5 +52,17 @@ } } } - +} + +.wp-admin { + .ppcp-button-applepay { + pointer-events: none; + } + &.ppcp-non-ios-device { + .ppcp-button-applepay { + apple-pay-button { + display: block; + } + } + } } diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index f9c188741..8c35c9eb5 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -4,6 +4,7 @@ import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hidin import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler'; import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator"; import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler'; +import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; class ApplepayButton { @@ -48,7 +49,7 @@ class ApplepayButton { const isEligible = this.applePayConfig.isEligible; if (isEligible) { this.fetchTransactionInfo().then(() => { - const isSubscriptionProduct = this.ppcpConfig.data_client_id.has_subscriptions === true; + const isSubscriptionProduct = this.ppcpConfig?.data_client_id?.has_subscriptions === true; if (isSubscriptionProduct) { return; } @@ -145,13 +146,12 @@ class ApplepayButton { return session; } - - - /** * Add a Apple Pay purchase button */ addButton() { + this.log('addButton', this.context); + const wrapper = (this.context === 'mini-cart') ? this.buttonConfig.button.mini_cart_wrapper @@ -160,7 +160,7 @@ class ApplepayButton { (this.context === 'mini-cart') ? this.ppcpConfig.button.mini_cart_style.shape : this.ppcpConfig.button.style.shape; - const appleContainer = this.context === 'mini-cart' ? document.getElementById("applepay-container-minicart") : document.getElementById("applepay-container"); + const appleContainer = document.getElementById(wrapper); const type = this.buttonConfig.button.type; const language = this.buttonConfig.button.lang; const color = this.buttonConfig.button.color; @@ -195,7 +195,7 @@ class ApplepayButton { try { const formData = new FormData(document.querySelector(checkoutFormSelector)); this.form_saved = Object.fromEntries(formData.entries()); - // This line should be reviewed, the paypal.Applepay().confirmOrder fails if we add it. + // This line should be reviewed, the widgetBuilder.paypal.Applepay().confirmOrder fails if we add it. //this.update_request_data_with_form(paymentDataRequest); } catch (error) { console.error(error); @@ -279,7 +279,7 @@ class ApplepayButton { return (applePayValidateMerchantEvent) => { this.log('onvalidatemerchant call'); - paypal.Applepay().validateMerchant({ + widgetBuilder.paypal.Applepay().validateMerchant({ validationUrl: applePayValidateMerchantEvent.validationURL }) .then(validateResult => { @@ -506,7 +506,7 @@ class ApplepayButton { this.log('onpaymentauthorized paypal order ID', id, event.payment.token, event.payment.billingContact); try { - const confirmOrderResponse = await paypal.Applepay().confirmOrder({ + const confirmOrderResponse = await widgetBuilder.paypal.Applepay().confirmOrder({ orderId: id, token: event.payment.token, billingContact: event.payment.billingContact, diff --git a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js index f08c2d6a1..3deb4a47e 100644 --- a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js @@ -65,6 +65,13 @@ class BaseHandler { ); } + errorHandler() { + return new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + } + } export default BaseHandler; diff --git a/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js b/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js index 04338354b..bea8e837b 100644 --- a/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js +++ b/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js @@ -4,6 +4,7 @@ import CheckoutHandler from "./CheckoutHandler"; import CartBlockHandler from "./CartBlockHandler"; import CheckoutBlockHandler from "./CheckoutBlockHandler"; import MiniCartHandler from "./MiniCartHandler"; +import PreviewHandler from "./PreviewHandler"; import PayNowHandler from "./PayNowHandler"; class ContextHandlerFactory { @@ -24,6 +25,8 @@ class ContextHandlerFactory { return new CartBlockHandler(buttonConfig, ppcpConfig); case 'checkout-block': return new CheckoutBlockHandler(buttonConfig, ppcpConfig); + case 'preview': + return new PreviewHandler(buttonConfig, ppcpConfig); } } } diff --git a/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js new file mode 100644 index 000000000..6e0da8852 --- /dev/null +++ b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js @@ -0,0 +1,37 @@ +import BaseHandler from "./BaseHandler"; + +class PreviewHandler extends BaseHandler { + + constructor(buttonConfig, ppcpConfig, externalHandler) { + super(buttonConfig, ppcpConfig, externalHandler); + } + + transactionInfo() { + // We need to return something as ApplePay button initialization expects valid data. + return { + countryCode: "US", + currencyCode: "USD", + totalPrice: "10.00", + totalPriceStatus: "FINAL" + } + } + + createOrder() { + throw new Error('Create order fail. This is just a preview.'); + } + + approveOrder(data, actions) { + throw new Error('Approve order fail. This is just a preview.'); + } + + actionHandler() { + throw new Error('Action handler fail. This is just a preview.'); + } + + errorHandler() { + throw new Error('Error handler fail. This is just a preview.'); + } + +} + +export default PreviewHandler; diff --git a/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js index 309694a77..c5d0549d2 100644 --- a/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js @@ -48,6 +48,14 @@ class SingleProductHandler extends BaseHandler { }); } + createOrder() { + return this.actionHandler().configuration().createOrder(null, null, { + 'updateCartOptions': { + 'keepShipping': true + } + }); + } + actionHandler() { return new SingleProductActionHandler( this.ppcpConfig, @@ -60,12 +68,9 @@ class SingleProductHandler extends BaseHandler { ); } - products() { return this.actionHandler().getProducts(); } - - } export default SingleProductHandler; diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js new file mode 100644 index 000000000..30032f56b --- /dev/null +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -0,0 +1,148 @@ +import {loadCustomScript} from "@paypal/paypal-js"; +import ApplepayButton from "./ApplepayButton"; +import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; + +(function ({ + buttonConfig, + jQuery +}) { + + let applePayConfig; + let buttonQueue = []; + let activeButtons = {}; + let bootstrapped = false; + + // React to PayPal config changes. + jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => { + if (bootstrapped) { + createButton(ppcpConfig); + } else { + buttonQueue.push({ + ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig)) + }); + } + }); + + // React to ApplePay config changes. + jQuery([ + '#ppcp-applepay_button_enabled', + '#ppcp-applepay_button_type', + '#ppcp-applepay_button_color', + '#ppcp-applepay_button_language' + ].join(',')).on('change', () => { + for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { + createButton(ppcpConfig); + } + }); + + // Maybe we can find a more elegant reload method when transitioning from styling modes. + jQuery([ + '#ppcp-smart_button_enable_styling_per_location' + ].join(',')).on('change', () => { + setTimeout(() => { + for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { + createButton(ppcpConfig); + } + }, 100); + }); + + const applyConfigOptions = function (buttonConfig) { + buttonConfig.button = buttonConfig.button || {}; + buttonConfig.button.type = jQuery('#ppcp-applepay_button_type').val(); + buttonConfig.button.color = jQuery('#ppcp-applepay_button_color').val(); + buttonConfig.button.lang = jQuery('#ppcp-applepay_button_language').val(); + } + + const createButton = function (ppcpConfig) { + const selector = ppcpConfig.button.wrapper + 'ApplePay'; + + if (!jQuery('#ppcp-applepay_button_enabled').is(':checked')) { + jQuery(selector).remove(); + return; + } + + buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); + buttonConfig.button.wrapper = selector.replace('#', ''); + applyConfigOptions(buttonConfig); + + const wrapperElement = `
`; + + if (!jQuery(selector).length) { + jQuery(ppcpConfig.button.wrapper).after(wrapperElement); + } else { + jQuery(selector).replaceWith(wrapperElement); + } + + const button = new ApplepayButton( + 'preview', + null, + buttonConfig, + ppcpConfig, + ); + + button.init(applePayConfig); + + activeButtons[selector] = ppcpConfig; + } + + const bootstrap = async function () { + if (!widgetBuilder.paypal) { + return; + } + + applePayConfig = await widgetBuilder.paypal.Applepay().config(); + + // We need to set bootstrapped here otherwise applePayConfig may not be set. + bootstrapped = true; + + let options; + while (options = buttonQueue.pop()) { + createButton(options.ppcpConfig); + } + + if (!window.ApplePaySession) { + jQuery('body').addClass('ppcp-non-ios-device') + } + }; + + document.addEventListener( + 'DOMContentLoaded', + () => { + + if (typeof (buttonConfig) === 'undefined') { + console.error('PayPal button could not be configured.'); + return; + } + + let paypalLoaded = false; + let applePayLoaded = false; + + const tryToBoot = () => { + if (!bootstrapped && paypalLoaded && applePayLoaded) { + bootstrap(); + } + } + + // Load ApplePay SDK + loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { + applePayLoaded = true; + tryToBoot(); + }); + + // Wait for PayPal to be loaded externally + if (typeof widgetBuilder.paypal !== 'undefined') { + paypalLoaded = true; + tryToBoot(); + } + + jQuery(document).on('ppcp-paypal-loaded', () => { + paypalLoaded = true; + tryToBoot(); + }); + }, + ); + +})({ + buttonConfig: window.wc_ppcp_applepay_admin, + jQuery: window.jQuery +}); diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index f345409e6..fce1eddda 100644 --- a/modules/ppcp-applepay/services.php +++ b/modules/ppcp-applepay/services.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton; use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; use WooCommerce\PayPalCommerce\Applepay\Assets\DataToAppleButtonScripts; use WooCommerce\PayPalCommerce\Applepay\Assets\BlocksPaymentMethod; +use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary; use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies; use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\Onboarding\Environment; @@ -56,11 +57,16 @@ return array( $container->get( 'wcgateway.is-ppcp-settings-page' ), $container->get( 'applepay.available' ) || ( ! $container->get( 'applepay.is_referral' ) ), $container->get( 'applepay.server_supported' ), - $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false, + $container->get( 'applepay.is_validated' ), $container->get( 'applepay.button' ) ); }, + 'applepay.is_validated' => static function ( ContainerInterface $container ): bool { + $settings = $container->get( 'wcgateway.settings' ); + return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false; + }, + 'applepay.apple-product-status' => static function( ContainerInterface $container ): AppleProductStatus { return new AppleProductStatus( $container->get( 'wcgateway.settings' ), @@ -83,6 +89,18 @@ return array( 'applepay.server_supported' => static function ( ContainerInterface $container ): bool { return ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off'; }, + 'applepay.is_browser_supported' => static function ( ContainerInterface $container ): bool { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $user_agent = wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ); + if ( $user_agent ) { + foreach ( PropertiesDictionary::ALLOWED_USER_AGENTS as $allowed_agent ) { + if ( strpos( $user_agent, $allowed_agent ) !== false ) { + return true; + } + } + } + return false; + }, 'applepay.url' => static function ( ContainerInterface $container ): string { $path = realpath( __FILE__ ); if ( false === $path ) { diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index d29bcc240..a02e26884 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -90,6 +90,8 @@ class ApplepayModule implements ModuleInterface { $module->render_buttons( $c, $apple_payment_method ); $apple_payment_method->bootstrap_ajax_request(); } + + $module->load_admin_assets( $c, $apple_payment_method ); }, 1 ); @@ -182,6 +184,43 @@ class ApplepayModule implements ModuleInterface { ); } + /** + * Registers and enqueues the assets. + * + * @param ContainerInterface $c The container. + * @param ApplePayButton $button The button. + * @return void + */ + public function load_admin_assets( ContainerInterface $c, ApplePayButton $button ): void { + // Enqueue backend scripts. + add_action( + 'admin_enqueue_scripts', + static function () use ( $c, $button ) { + if ( ! is_admin() ) { + return; + } + + /** + * Should add this to the ButtonInterface. + * + * @psalm-suppress UndefinedInterfaceMethod + */ + $button->enqueue_admin(); + } + ); + + // Adds ApplePay component to the backend button preview settings. + add_action( + 'woocommerce_paypal_payments_admin_gateway_settings', + function( array $settings ) use ( $c ): array { + if ( is_array( $settings['components'] ) ) { + $settings['components'][] = 'applepay'; + } + return $settings; + } + ); + } + /** * Renders the Apple Pay buttons in the enabled places. * diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index fe7313a85..da909f677 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -942,7 +942,7 @@ class ApplePayButton implements ButtonInterface { } if ( $button_enabled_cart ) { $default_hook_name = 'woocommerce_paypal_payments_cart_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_cart_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_cart_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -954,7 +954,7 @@ class ApplePayButton implements ButtonInterface { if ( $button_enabled_checkout ) { $default_hook_name = 'woocommerce_paypal_payments_checkout_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_checkout_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_checkout_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -966,7 +966,7 @@ class ApplePayButton implements ButtonInterface { } if ( $button_enabled_payorder ) { $default_hook_name = 'woocommerce_paypal_payments_payorder_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_payorder_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_payorder_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -979,7 +979,7 @@ class ApplePayButton implements ButtonInterface { if ( $button_enabled_minicart ) { $default_hook_name = 'woocommerce_paypal_payments_minicart_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_minicart_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_minicart_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -1059,6 +1059,34 @@ class ApplePayButton implements ButtonInterface { wp_enqueue_style( 'wc-ppcp-applepay' ); } + /** + * Enqueues scripts/styles for admin. + */ + public function enqueue_admin(): void { + wp_register_style( + 'wc-ppcp-applepay-admin', + untrailingslashit( $this->module_url ) . '/assets/css/styles.css', + array(), + $this->version + ); + wp_enqueue_style( 'wc-ppcp-applepay-admin' ); + + wp_register_script( + 'wc-ppcp-applepay-admin', + untrailingslashit( $this->module_url ) . '/assets/js/boot-admin.js', + array(), + $this->version, + true + ); + wp_enqueue_script( 'wc-ppcp-applepay-admin' ); + + wp_localize_script( + 'wc-ppcp-applepay-admin', + 'wc_ppcp_applepay_admin', + $this->script_data_for_admin() + ); + } + /** * Returns the script data. * @@ -1068,6 +1096,15 @@ class ApplePayButton implements ButtonInterface { return $this->script_data->apple_pay_script_data(); } + /** + * Returns the admin script data. + * + * @return array + */ + public function script_data_for_admin(): array { + return $this->script_data->apple_pay_script_data_for_admin(); + } + /** * Returns true if the module is enabled. * diff --git a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php index 882776cf1..afe0126ef 100644 --- a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php +++ b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php @@ -69,6 +69,26 @@ class DataToAppleButtonScripts { ); } + + /** + * Returns the appropriate admin data to send to ApplePay script + * + * @return array + * @throws NotFoundException When the setting is not found. + */ + public function apple_pay_script_data_for_admin(): array { + $base_location = wc_get_base_location(); + $shop_country_code = $base_location['country']; + $currency_code = get_woocommerce_currency(); + $total_label = get_bloginfo( 'name' ); + + return $this->data_for_admin_page( + $shop_country_code, + $currency_code, + $total_label + ); + } + /** * Check if the product needs shipping * @@ -199,4 +219,47 @@ class DataToAppleButtonScripts { 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), ); } + + /** + * Prepares the data for the cart page. + * Consider refactoring this method along with data_for_cart_page() and data_for_product_page() methods. + * + * @param string $shop_country_code The shop country code. + * @param string $currency_code The currency code. + * @param string $total_label The label for the total amount. + * + * @return array + */ + protected function data_for_admin_page( + $shop_country_code, + $currency_code, + $total_label + ) { + $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; + $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; + $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; + $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); + + return array( + 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, + 'button' => array( + 'wrapper' => 'applepay-container', + 'mini_cart_wrapper' => 'applepay-container-minicart', + 'type' => $type, + 'color' => $color, + 'lang' => $lang, + ), + 'product' => array( + 'needShipping' => false, + 'subtotal' => 0, + ), + 'shop' => array( + 'countryCode' => $shop_country_code, + 'currencyCode' => $currency_code, + 'totalLabel' => $total_label, + ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + ); + } } diff --git a/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php b/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php index 25227e11b..d1c388ffd 100644 --- a/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php +++ b/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php @@ -13,6 +13,7 @@ namespace WooCommerce\PayPalCommerce\Applepay\Assets; * Class PropertiesDictionary */ class PropertiesDictionary { + public const ALLOWED_USER_AGENTS = array( 'Safari', 'Macintosh', 'iPhone', 'iPad', 'iPod' ); public const BILLING_CONTACT_INVALID = 'billing Contact Invalid'; diff --git a/modules/ppcp-applepay/webpack.config.js b/modules/ppcp-applepay/webpack.config.js index 63834c8a6..94e7bfd8b 100644 --- a/modules/ppcp-applepay/webpack.config.js +++ b/modules/ppcp-applepay/webpack.config.js @@ -11,6 +11,7 @@ module.exports = { entry: { 'boot': path.resolve('./resources/js/boot.js'), 'boot-block': path.resolve('./resources/js/boot-block.js'), + 'boot-admin': path.resolve('./resources/js/boot-admin.js'), "styles": path.resolve('./resources/css/styles.scss') }, output: { diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index 42f746b5f..9590e9a57 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -143,7 +143,7 @@ class SingleProductActionHandler { { this.cartHelper = null; - return (data, actions) => { + return (data, actions, options = {}) => { this.errorHandler.clear(); const onResolve = (purchase_units) => { @@ -178,7 +178,7 @@ class SingleProductActionHandler { }); }; - return this.updateCart.update(onResolve, this.getProducts()); + return this.updateCart.update(onResolve, this.getProducts(), options.updateCartOptions || {}); }; } diff --git a/modules/ppcp-button/resources/js/modules/Helper/UpdateCart.js b/modules/ppcp-button/resources/js/modules/Helper/UpdateCart.js index 4c5b08355..f79f8613b 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/UpdateCart.js +++ b/modules/ppcp-button/resources/js/modules/Helper/UpdateCart.js @@ -11,9 +11,10 @@ class UpdateCart { * * @param onResolve * @param {Product[]} products + * @param {Object} options * @returns {Promise} */ - update(onResolve, products) + update(onResolve, products, options = {}) { return new Promise((resolve, reject) => { fetch( @@ -27,6 +28,7 @@ class UpdateCart { body: JSON.stringify({ nonce: this.nonce, products, + ...options }) } ).then( diff --git a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php index 701076bed..62ad02568 100644 --- a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php @@ -71,6 +71,8 @@ class ChangeCartEndpoint extends AbstractCartEndpoint { * @throws Exception On error. */ protected function handle_data(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + $this->cart_products->set_cart( $this->cart ); $products = $this->products_from_request(); @@ -79,7 +81,9 @@ class ChangeCartEndpoint extends AbstractCartEndpoint { return false; } - $this->shipping->reset_shipping(); + if ( ! ( $data['keepShipping'] ?? false ) ) { + $this->shipping->reset_shipping(); + } if ( ! $this->add_products( $products ) ) { return false; diff --git a/modules/ppcp-compat/src/Assets/CompatAssets.php b/modules/ppcp-compat/src/Assets/CompatAssets.php index 85e0bde7b..34b038048 100644 --- a/modules/ppcp-compat/src/Assets/CompatAssets.php +++ b/modules/ppcp-compat/src/Assets/CompatAssets.php @@ -107,8 +107,8 @@ class CompatAssets { 'ppcp-tracking-compat', 'PayPalCommerceGatewayOrderTrackingCompat', array( - 'gzd_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) && $this->is_gzd_active, - 'wc_shipment_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) && $this->is_wc_shipment_active, + 'gzd_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) && $this->is_gzd_active, + 'wc_shipment_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) && $this->is_wc_shipment_active, 'wc_shipping_tax_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_wc_shipping_tax', true ) && $this->is_wc_shipping_tax_active, ) ); diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index c7f8a80a1..f8233a410 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -44,3 +44,9 @@ } } + +.wp-admin { + .ppcp-button-googlepay { + pointer-events: none; + } +} diff --git a/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js index a637f078d..f4eeee486 100644 --- a/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js @@ -1,6 +1,6 @@ import BaseHandler from "./BaseHandler"; -class CartHandler extends BaseHandler { +class PreviewHandler extends BaseHandler { constructor(buttonConfig, ppcpConfig, externalHandler) { super(buttonConfig, ppcpConfig, externalHandler); @@ -28,4 +28,4 @@ class CartHandler extends BaseHandler { } -export default CartHandler; +export default PreviewHandler; diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js index a014f2ae6..6760cfb80 100644 --- a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js @@ -48,6 +48,14 @@ class SingleProductHandler extends BaseHandler { }); } + createOrder() { + return this.actionHandler().configuration().createOrder(null, null, { + 'updateCartOptions': { + 'keepShipping': true + } + }); + } + actionHandler() { return new SingleProductActionHandler( this.ppcpConfig, diff --git a/modules/ppcp-onboarding/resources/css/onboarding.scss b/modules/ppcp-onboarding/resources/css/onboarding.scss index 97cb65e66..bd8c20298 100644 --- a/modules/ppcp-onboarding/resources/css/onboarding.scss +++ b/modules/ppcp-onboarding/resources/css/onboarding.scss @@ -92,6 +92,10 @@ ul.ppcp-onboarding-options-sublist { margin: 5px; } +.ppcp-onboarding-header-apm-logos img { + margin: 3px; +} + .ppcp-onboarding-header-cards img { height: 37px; } @@ -99,6 +103,9 @@ ul.ppcp-onboarding-options-sublist { .ppcp-onboarding-header-paypal-logos img { height: 32px; } +.ppcp-onboarding-header-apm-logos img { + height: 28px; +} .ppcp-onboarding-cards-options table { margin-left: 35px; @@ -158,7 +165,11 @@ ul.ppcp-onboarding-options-sublist { } .ppcp-onboarding-header-cards img, .ppcp-onboarding-header-paypal-logos img { - margin: 2px; + margin: 2.5px; + } + + .ppcp-onboarding-header-apm-logos img { + margin: 3px; } .ppcp-onboarding-header-cards img { @@ -168,6 +179,9 @@ ul.ppcp-onboarding-options-sublist { .ppcp-onboarding-header-paypal-logos img { height: 23px; } + .ppcp-onboarding-header-apm-logos img { + height: 19.8px; + } } .ppcp-settings-page-header { diff --git a/modules/ppcp-wc-gateway/assets/images/button-Apple-Pay.png b/modules/ppcp-wc-gateway/assets/images/button-Apple-Pay.png new file mode 100644 index 000000000..86de5e418 Binary files /dev/null and b/modules/ppcp-wc-gateway/assets/images/button-Apple-Pay.png differ diff --git a/modules/ppcp-wc-gateway/assets/images/button-Google-Pay.png b/modules/ppcp-wc-gateway/assets/images/button-Google-Pay.png new file mode 100644 index 000000000..8e836b203 Binary files /dev/null and b/modules/ppcp-wc-gateway/assets/images/button-Google-Pay.png differ diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 067acf434..4a7e3ac6a 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -55,17 +55,21 @@ return function ( ContainerInterface $container, array $fields ): array {
- PayPal - Venmo - Pay Later + PayPal + Venmo + Pay Later
- Visa - Mastercard - American Express - Discover - iDEAL - Sofort + Visa + Mastercard + American Express + Discover + iDEAL + Sofort +
+
+ Apple Pay + Google Pay
', diff --git a/package.json b/package.json index c18090e0f..bef908fec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.4.0", + "version": "2.4.1", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index 7df738d23..db1e38b34 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.3 Requires PHP: 7.2 -Stable tag: 2.4.0 +Stable tag: 2.4.1 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -180,6 +180,26 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == += 2.4.1 - xxxx-xx-xx = +* Fix - Error "PayPal order ID not found in meta" prevents automations from triggering when buying subscription via third-party payment gateway #1822 +* Fix - Card button subscription support declaration #1796 +* Fix - Pay Later messaging disappears when updating shipping option on cart page #1807 +* Fix - Apple Pay payment from single product may fail after changing shipping options in Apple Pay payment sheet #1810 +* Enhancement - Extend list of supported countries for Advanced Card Processing #1808 +* Enhancement - Extend Apple Pay/Google Pay country eligibility to Italy #1811 +* Enhancement - Override language used to display PayPal buttons #600 +* Enhancement - Apple Pay button preview #1824 +* Enhancement - Add Apple Pay & Google Pay logos on the onboarding page #1823 +* Enhancement - Improve Apple Pay compatibility with variable products on single product page #1803 +* Enhancement - Apple Pay domain registration & browser eligibility check #1821 +* Enhancement - Package Tracking compatibility with WooCommerce Shipping & ShipStation for WooCommerce #1813 +* Enhancement - Fill form when continuation in block #1794 +* Enhancement - Display Shop location Pay Later messaging on product category pages #1809 +* Enhancement - Present apple-developer-merchantid-domain-association file only when Apple Pay is enabled #1818 +* Enhancement - Improve Apple Pay compatibility on Pay for Order page #1815 +* Enhancement - Display Pay Later messages before the payment methods on the Pay for Order page #1814 +* Enhancement - Handle undefined array key warnings on PHP 8.1 #1804 + = 2.4.0 - 2023-10-31 = * Fix - Mini-Cart Bug cause of wrong DOM-Structure in v2.3.1 #1735 * Fix - ACDC disappearing after plugin updates #1751 diff --git a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php index 427e9ce79..dbff4d5c3 100644 --- a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php @@ -80,6 +80,7 @@ class ChangeCartEndpointTest extends TestCase $requestData = Mockery::mock(RequestData::class); $requestData ->expects('read_request') + ->times(2) ->with(ChangeCartEndpoint::nonce()) ->andReturn($data); diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 2092a9cee..5e89cbae4 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,7 +3,7 @@ * 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.4.0 + * Version: 2.4.1 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 @@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2023-10-25' ); +define( 'PAYPAL_INTEGRATION_DATE', '2023-11-06' ); ! 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_' );