From 737aca04b36720ca54acb7377c65e954dba5c0de Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Mon, 9 Dec 2024 11:20:55 +1300 Subject: [PATCH 01/26] Fix error log messages incorrecty categorized as info (#9890) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> --- changelog/fix-9889-log-level | 4 ++++ includes/class-logger.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-9889-log-level diff --git a/changelog/fix-9889-log-level b/changelog/fix-9889-log-level new file mode 100644 index 00000000000..d2f54e24c1a --- /dev/null +++ b/changelog/fix-9889-log-level @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Errors were incorrectly marked as info in logs. diff --git a/includes/class-logger.php b/includes/class-logger.php index 3384d0fe443..1ce8ae255ab 100644 --- a/includes/class-logger.php +++ b/includes/class-logger.php @@ -36,7 +36,7 @@ class Logger { * 'debug': Debug-level messages. */ public static function log( $message, $level = 'info' ) { - wcpay_get_container()->get( InternalLogger::class )->log( $message ); + wcpay_get_container()->get( InternalLogger::class )->log( $message, $level ); } /** From 15e7ef8c89f87abf07cd6c748a39a98a822ef934 Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Mon, 9 Dec 2024 16:17:48 +0100 Subject: [PATCH 02/26] Add check for `wc_get_payment_gateway_by_order` (#9903) Co-authored-by: Marcin Bot --- changelog/fix-add-payment-method-check | 5 +++++ includes/class-woopay-tracker.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-add-payment-method-check diff --git a/changelog/fix-add-payment-method-check b/changelog/fix-add-payment-method-check new file mode 100644 index 00000000000..4ffc9e6342f --- /dev/null +++ b/changelog/fix-add-payment-method-check @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Added a check for the gateway id before comparing it + + diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index 538ec873dc8..fbcdd6bc948 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -543,7 +543,7 @@ public function checkout_order_processed( $order_id ) { $properties = [ 'payment_title' => 'other' ]; // If the order was placed using WooCommerce Payments, record the payment title using Tracks. - if ( strpos( $payment_gateway->id, 'woocommerce_payments' ) === 0 ) { + if ( isset( $payment_gateway->id ) && strpos( $payment_gateway->id, 'woocommerce_payments' ) === 0 ) { $order = wc_get_order( $order_id ); $payment_title = $order->get_payment_method_title(); $properties = [ 'payment_title' => $payment_title ]; From 0ea8272f82e760b0648c072d0307b1bc76582f90 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 10 Dec 2024 14:54:01 +0100 Subject: [PATCH 03/26] fix: console warning for useEffect on plugins page (#9913) --- changelog/fix-use-effect-console-warning | 5 +++++ client/plugins-page/index.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-use-effect-console-warning diff --git a/changelog/fix-use-effect-console-warning b/changelog/fix-use-effect-console-warning new file mode 100644 index 00000000000..45219e7b39a --- /dev/null +++ b/changelog/fix-use-effect-console-warning @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: fix: console warning on plugins page + + diff --git a/client/plugins-page/index.js b/client/plugins-page/index.js index 24d59e65fa5..b960794f65d 100644 --- a/client/plugins-page/index.js +++ b/client/plugins-page/index.js @@ -77,12 +77,12 @@ const PluginsPage = () => { useEffect( () => { // If the survey is dismissed skip event listeners. if ( isModalDismissed() ) { - return null; + return; } // Abort if the deactivation link is not present. if ( deactivationLink === null ) { - return null; + return; } // Handle click event. From adbac29236168227b9e191a6c7ac4a15a2018d32 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 10 Dec 2024 18:41:04 +0100 Subject: [PATCH 04/26] Update confirmation modal after progressive onboarding (#9915) --- changelog/update-confirmation-modal-nox | 4 + .../index.tsx | 98 +++++++++---------- .../style.scss | 85 +++++++++++----- .../test/index.test.tsx | 4 +- client/overview/task-list/strings.tsx | 5 +- client/overview/test/index.js | 2 +- 6 files changed, 116 insertions(+), 82 deletions(-) create mode 100644 changelog/update-confirmation-modal-nox diff --git a/changelog/update-confirmation-modal-nox b/changelog/update-confirmation-modal-nox new file mode 100644 index 00000000000..0ffd1af6127 --- /dev/null +++ b/changelog/update-confirmation-modal-nox @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update confirmation modal after onbarding diff --git a/client/overview/modal/progressive-onboarding-eligibility/index.tsx b/client/overview/modal/progressive-onboarding-eligibility/index.tsx index d4b3f79021f..6f6be89a707 100644 --- a/client/overview/modal/progressive-onboarding-eligibility/index.tsx +++ b/client/overview/modal/progressive-onboarding-eligibility/index.tsx @@ -5,8 +5,9 @@ import React, { useEffect, useState } from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { Button, Modal } from '@wordpress/components'; -import { Icon, store, widget, tool } from '@wordpress/icons'; +import { Icon, store, currencyDollar } from '@wordpress/icons'; import { useDispatch } from '@wordpress/data'; +import interpolateComponents from '@automattic/interpolate-components'; /** * Internal dependencies @@ -59,75 +60,74 @@ const ProgressiveOnboardingEligibilityModal: React.FC = () => { setModalVisible( false ); }; - // Workaround to remove Modal header from the modal until `hideHeader` prop can be used. - useEffect( () => { - document - .querySelector( - '.wcpay-progressive-onboarding-eligibility-modal .components-modal__header-heading-container' - ) - ?.remove(); - }, [] ); - if ( ! modalVisible || modalDismissed ) return null; return ( -

- { __( 'You’re ready to sell.', 'woocommerce-payments' ) } -

- { __( - 'Start selling now and fast track the setup process, or continue the process to set up payouts with WooPayments.', - 'woocommerce-payments' - ) } + { interpolateComponents( { + mixedString: sprintf( + __( + 'Great news — your %s account has been activated. You can now start accepting payments on your store, subject to {{restrictionsLink}}certain restrictions{{/restrictionsLink}}.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + restrictionsLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) }

- -

+ +
+

+ { __( + 'Start selling instantly', + 'woocommerce-payments' + ) } +

{ __( - 'Start selling instantly', + 'You have 30 days from your first transaction or until you reach $5,000 in sales to verify your information and set up payouts.', 'woocommerce-payments' ) } -

- { sprintf( - /* translators: %s: WooPayments */ - __( - '%s enables you to start processing credit card payments right away.', - 'woocommerce-payments' - ), - 'WooPayments' - ) } -
-
- -

- { __( 'Quick and easy setup', 'woocommerce-payments' ) } -

- { __( - 'The setup process is super simple and ensures your store is ready to accept card payments.', - 'woocommerce-payments' - ) } +
- -

- { __( 'Flexible process', 'woocommerce-payments' ) } -

- { __( - 'You have a $5,000 balance limit or 30 days from your first transaction to verify and set up payouts in your account.', - 'woocommerce-payments' - ) } + +
+

+ { __( + 'Start receiving payouts', + 'woocommerce-payments' + ) } +

+ { __( + 'Provide some additional details about your business so you can continue accepting payments and begin receiving payouts without restrictions.', + 'woocommerce-payments' + ) } +
diff --git a/client/components/sandbox-mode-switch-to-live-notice/modal/style.scss b/client/components/sandbox-mode-switch-to-live-notice/modal/style.scss index b4067be8ba1..ff2b10db5f0 100644 --- a/client/components/sandbox-mode-switch-to-live-notice/modal/style.scss +++ b/client/components/sandbox-mode-switch-to-live-notice/modal/style.scss @@ -1,21 +1,30 @@ .wcpay-setup-real-payments-modal { - color: $gray-900; - fill: $studio-woocommerce-purple-50; + &.components-modal__frame { + width: 512px; + + @media screen and ( max-width: $break-small ) { + height: fit-content; + margin: auto auto; + max-width: 90vw; + } + } .components-modal__content { box-sizing: border-box; max-width: 600px; - margin: auto; + margin: 0; padding: $gap-smaller $gap-larger $gap-larger; } .components-modal__header { position: initial; - padding: 0; + padding: 24px 0 16px 0; + height: auto; border: 0; h1 { @include wp-title-small; + font-weight: 300; margin-bottom: $gap-smaller; } } @@ -24,20 +33,36 @@ @include wp-title-small; } - &__headline { - font-weight: 600; - } - &__content { - display: grid; - grid-template-columns: auto 1fr; - gap: $gap; - padding: $gap-smallest; - align-items: center; - margin-bottom: $gap-large; + display: flex; + gap: $gap-large; + flex-direction: column; + padding: $gap-small 0 $gap 0; + + &__item { + p { + line-height: 20px; + margin: 0; + } + } + + &__item-flex { + display: flex; + gap: $gap; + padding-right: $gap-large; + + &__description { + color: $gray-700; + } + p { + line-height: 20px; + margin: 0; + } + } } &__footer { @include modal-footer-buttons; + padding-top: $gap-large; } } diff --git a/client/components/sandbox-mode-switch-to-live-notice/modal/test/index.test.tsx b/client/components/sandbox-mode-switch-to-live-notice/modal/test/index.test.tsx index 2f82a5a263b..e2341aaa3a5 100644 --- a/client/components/sandbox-mode-switch-to-live-notice/modal/test/index.test.tsx +++ b/client/components/sandbox-mode-switch-to-live-notice/modal/test/index.test.tsx @@ -36,7 +36,7 @@ describe( 'Setup Live Payments Modal', () => { expect( screen.queryByText( - 'Before proceeding, please take note of the following information:' + "Before continuing, please make sure that you're aware of the following:" ) ).toBeInTheDocument(); } ); @@ -58,7 +58,7 @@ describe( 'Setup Live Payments Modal', () => { user.click( screen.getByRole( 'button', { - name: 'Continue setup', + name: 'Activate payments', } ) ); diff --git a/client/components/sandbox-mode-switch-to-live-notice/style.scss b/client/components/sandbox-mode-switch-to-live-notice/style.scss new file mode 100644 index 00000000000..0171d296b0a --- /dev/null +++ b/client/components/sandbox-mode-switch-to-live-notice/style.scss @@ -0,0 +1,5 @@ +.sandbox-mode-notice { + .wcpay-banner-notice__content { + display: flex; + } +} From 179f84ebbdddec992f10b2aea9164e78de4f83dd Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 12 Dec 2024 13:34:56 +0100 Subject: [PATCH 09/26] chore: remove ECE error assignment on loaderror (#9935) --- changelog/chore-remove-ece-error-assignment-on-loaderror | 5 +++++ client/express-checkout/index.js | 4 ---- client/tokenized-express-checkout/index.js | 4 ---- 3 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 changelog/chore-remove-ece-error-assignment-on-loaderror diff --git a/changelog/chore-remove-ece-error-assignment-on-loaderror b/changelog/chore-remove-ece-error-assignment-on-loaderror new file mode 100644 index 00000000000..cce991d09ba --- /dev/null +++ b/changelog/chore-remove-ece-error-assignment-on-loaderror @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: chore: remove ECE error assignment on loaderror + + diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index 6f36a3e6b59..0e2239946b6 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -256,10 +256,6 @@ jQuery( ( $ ) => { expressCheckoutButtonUi.renderButton( eceButton ); eceButton.on( 'loaderror', () => { - wcPayECEError = __( - 'The cart is incompatible with express checkout.', - 'woocommerce-payments' - ); if ( ! document.getElementById( 'wcpay-woopay-button' ) ) { expressCheckoutButtonUi.getButtonSeparator().hide(); } diff --git a/client/tokenized-express-checkout/index.js b/client/tokenized-express-checkout/index.js index 940aa1462b8..d18cb9f5be5 100644 --- a/client/tokenized-express-checkout/index.js +++ b/client/tokenized-express-checkout/index.js @@ -235,10 +235,6 @@ jQuery( ( $ ) => { expressCheckoutButtonUi.renderButton( eceButton ); eceButton.on( 'loaderror', () => { - wcPayECEError = __( - 'The cart is incompatible with express checkout.', - 'woocommerce-payments' - ); if ( ! document.getElementById( 'wcpay-woopay-button' ) ) { expressCheckoutButtonUi.getButtonSeparator().hide(); } From 9d22a75478075c9bc438275b8aeb305b51260073 Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Thu, 12 Dec 2024 13:49:58 +0100 Subject: [PATCH 10/26] Move from docker-based to standalone Jurassic Tube setup (#9839) Co-authored-by: Timur Karimov --- .gitignore | 3 +++ bin/jurassic-tube-setup.sh | 43 ++++++++++++++++++++++--------- changelog/update-to-standalone-jt | 4 +++ package.json | 4 +-- 4 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 changelog/update-to-standalone-jt diff --git a/.gitignore b/.gitignore index d68b7c107c3..8a1b9da0119 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,6 @@ tests/e2e-pw/playwright/.cache/ tests/e2e-pw/tests/e2e-pw/.auth/* # Slate docs docs/rest-api/build/* + +# Jurassic Tube files +bin/jurassictube/ diff --git a/bin/jurassic-tube-setup.sh b/bin/jurassic-tube-setup.sh index 2859938e43e..aa7b2ec6fd3 100755 --- a/bin/jurassic-tube-setup.sh +++ b/bin/jurassic-tube-setup.sh @@ -3,29 +3,32 @@ # Exit if any command fails. set -e -echo "Checking if ${PWD}/docker/bin/jt directory exists..." +# Define Jurassic Tube directory using bin directory +JT_DIR="${PWD}/bin/jurassictube" -if [ -d "${PWD}/docker/bin/jt" ]; then - echo "${PWD}/docker/bin/jt already exists." +echo "Checking if ${JT_DIR} directory exists..." + +if [ -d "${JT_DIR}" ]; then + echo "${JT_DIR} already exists." else - echo "Creating ${PWD}/docker/bin/jt directory..." - mkdir -p "${PWD}/docker/bin/jt" + echo "Creating ${JT_DIR} directory..." + mkdir -p "${JT_DIR}" fi -echo "Downloading the latest version of the installer script..." +echo "Checking if the installer is present and downloading it if not..." echo # Download the installer (if it's not already present): -if [ ! -f "${PWD}/docker/bin/jt/installer.sh" ]; then - # Download the installer script: - curl "https://jurassic.tube/get-installer.php?env=wcpay" -o ${PWD}/docker/bin/jt/installer.sh && chmod +x ${PWD}/docker/bin/jt/installer.sh +if [ ! -f "${JT_DIR}/installer.sh" ]; then + echo "Downloading the standalone installer..." + curl "https://jurassic.tube/installer-standalone.sh" -o "${JT_DIR}/installer.sh" && chmod +x "${JT_DIR}/installer.sh" fi echo "Running the installation script..." echo # Run the installer script -source $PWD/docker/bin/jt/installer.sh +"${JT_DIR}/installer.sh" echo read -p "Go to https://jurassic.tube/ in a browser, paste your public key which was printed above into the box, and click 'Add Public Key'. Press enter to continue" @@ -40,8 +43,24 @@ echo read -p "Please enter your Automattic/WordPress.com username: " username echo -${PWD}/docker/bin/jt/config.sh username ${username} -${PWD}/docker/bin/jt/config.sh subdomain ${subdomain} +if [ ! -f "${JT_DIR}/config.env" ]; then + touch "${JT_DIR}/config.env" +else + > "${JT_DIR}/config.env" +fi + +# Find the WordPress container section and get its port +PORT=$(docker ps | grep woocommerce_payments_wordpress | sed -En "s/.*0:([0-9]+).*/\1/p") + +# Use default if extraction failed +if [ -z "$PORT" ]; then + PORT=8082 # Default fallback + echo "Could not extract WordPress container port, using default: ${PORT}" +fi + +echo "username=${username}" >> "${JT_DIR}/config.env" +echo "subdomain=${subdomain}" >> "${JT_DIR}/config.env" +echo "localhost=localhost:${PORT}" >> "${JT_DIR}/config.env" echo "Setup complete!" echo "Use the command: npm run tube:start from the root directory of your WC Payments project to start running Jurassic Tube." diff --git a/changelog/update-to-standalone-jt b/changelog/update-to-standalone-jt new file mode 100644 index 00000000000..4df87f235ec --- /dev/null +++ b/changelog/update-to-standalone-jt @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update the tunelling setup. diff --git a/package.json b/package.json index 4fa803a245c..d69725ac2da 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "format:css": "npm run format:provided '**/*.scss' '**/*.css'", "format:provided": "prettier --write", "tube:setup": "./bin/jurassic-tube-setup.sh", - "tube:start": "./docker/bin/jt/tunnel.sh", - "tube:stop": "./docker/bin/jt/tunnel.sh break", + "tube:start": "source ./bin/jurassictube/config.env && jurassictube -u \"$username\" -s \"$subdomain\" -h \"$localhost\"", + "tube:stop": "source ./bin/jurassictube/config.env && jurassictube -b -s \"$subdomain\"", "psalm": "./bin/run-psalm.sh", "xdebug:toggle": "docker compose exec -u root wordpress /var/www/html/wp-content/plugins/woocommerce-payments/bin/xdebug-toggle.sh", "changelog": "./vendor/bin/changelogger add", From f08a72744661194b5cef15fb298d07609237f5d1 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 12 Dec 2024 15:38:45 +0100 Subject: [PATCH 11/26] feat: tokenized ECE product page compatibility (#9877) --- ...nized-ece-product-page-base-implementation | 5 + .../compatibility/wc-deposits.js | 32 +- .../compatibility/wc-order-attribution.js | 4 +- ...oduct-variations.js => wc-product-page.js} | 46 +- .../debounce.js | 0 .../event-handlers.js | 2 +- client/tokenized-express-checkout/index.js | 449 +++++++----------- 7 files changed, 231 insertions(+), 307 deletions(-) create mode 100644 changelog/feat-tokenized-ece-product-page-base-implementation rename client/tokenized-express-checkout/compatibility/{wc-product-variations.js => wc-product-page.js} (62%) rename client/{tokenized-payment-request => tokenized-express-checkout}/debounce.js (100%) diff --git a/changelog/feat-tokenized-ece-product-page-base-implementation b/changelog/feat-tokenized-ece-product-page-base-implementation new file mode 100644 index 00000000000..e0f342c1623 --- /dev/null +++ b/changelog/feat-tokenized-ece-product-page-base-implementation @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: feat: tokenized ECE product page base implementation + + diff --git a/client/tokenized-express-checkout/compatibility/wc-deposits.js b/client/tokenized-express-checkout/compatibility/wc-deposits.js index 352b498b4b2..7993d08db78 100644 --- a/client/tokenized-express-checkout/compatibility/wc-deposits.js +++ b/client/tokenized-express-checkout/compatibility/wc-deposits.js @@ -1,15 +1,33 @@ /* global jQuery */ +/** + * External dependencies + */ +import { addFilter, doAction } from '@wordpress/hooks'; + jQuery( ( $ ) => { - // WooCommerce Deposits support. - // Trigger the "woocommerce_variation_has_changed" event when the deposit option is changed. $( 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' ).on( 'change', () => { - $( 'form' ) - .has( - 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' - ) - .trigger( 'woocommerce_variation_has_changed' ); + doAction( 'wcpay.express-checkout.update-button-data' ); } ); } ); +addFilter( + 'wcpay.express-checkout.cart-add-item', + 'automattic/wcpay/express-checkout', + ( productData ) => { + const depositsData = {}; + if ( jQuery( 'input[name=wc_deposit_option]' ).length ) { + depositsData.wc_deposit_option = jQuery( + 'input[name=wc_deposit_option]:checked' + ).val(); + } + if ( jQuery( 'input[name=wc_deposit_payment_plan]' ).length ) { + depositsData.wc_deposit_payment_plan = jQuery( + 'input[name=wc_deposit_payment_plan]:checked' + ).val(); + } + + return { ...productData, ...depositsData }; + } +); diff --git a/client/tokenized-express-checkout/compatibility/wc-order-attribution.js b/client/tokenized-express-checkout/compatibility/wc-order-attribution.js index a707f8330ab..96133d25559 100644 --- a/client/tokenized-express-checkout/compatibility/wc-order-attribution.js +++ b/client/tokenized-express-checkout/compatibility/wc-order-attribution.js @@ -6,8 +6,8 @@ import { addFilter } from '@wordpress/hooks'; addFilter( - 'wcpay.payment-request.cart-place-order-extension-data', - 'automattic/wcpay/payment-request', + 'wcpay.express-checkout.cart-place-order-extension-data', + 'automattic/wcpay/express-checkout', ( extensionData ) => { const orderAttributionValues = jQuery( '#wcpay-express-checkout__order-attribution-inputs input' diff --git a/client/tokenized-express-checkout/compatibility/wc-product-variations.js b/client/tokenized-express-checkout/compatibility/wc-product-page.js similarity index 62% rename from client/tokenized-express-checkout/compatibility/wc-product-variations.js rename to client/tokenized-express-checkout/compatibility/wc-product-page.js index 775a894fec4..3c242f046cf 100644 --- a/client/tokenized-express-checkout/compatibility/wc-product-variations.js +++ b/client/tokenized-express-checkout/compatibility/wc-product-page.js @@ -1,31 +1,41 @@ /* global jQuery */ +/** + * Internal dependencies + */ +import expressCheckoutButtonUi from '../button-ui'; +import debounce from '../debounce'; /** * External dependencies */ -import { addFilter, applyFilters } from '@wordpress/hooks'; -import paymentRequestButtonUi from '../button-ui'; +import { addFilter, doAction } from '@wordpress/hooks'; jQuery( ( $ ) => { $( document.body ).on( 'woocommerce_variation_has_changed', async () => { - try { - paymentRequestButtonUi.blockButton(); - - await applyFilters( - 'wcpay.payment-request.update-button-data', - Promise.resolve() - ); - - paymentRequestButtonUi.unblockButton(); - } catch ( e ) { - paymentRequestButtonUi.hide(); - } + doAction( 'wcpay.express-checkout.update-button-data' ); } ); } ); +// Block the payment request button as soon as an "input" event is fired, to avoid sync issues +// when the customer clicks on the button before the debounced event is processed. +jQuery( ( $ ) => { + const $quantityInput = $( '.quantity' ); + const handleQuantityChange = () => { + expressCheckoutButtonUi.blockButton(); + }; + $quantityInput.on( 'input', '.qty', handleQuantityChange ); + $quantityInput.on( + 'input', + '.qty', + debounce( 250, async () => { + doAction( 'wcpay.express-checkout.update-button-data' ); + } ) + ); +} ); + addFilter( - 'wcpay.payment-request.cart-add-item', - 'automattic/wcpay/payment-request', + 'wcpay.express-checkout.cart-add-item', + 'automattic/wcpay/express-checkout', ( productData ) => { const $variationInformation = jQuery( '.single_variation_wrap' ); if ( ! $variationInformation.length ) { @@ -42,8 +52,8 @@ addFilter( } ); addFilter( - 'wcpay.payment-request.cart-add-item', - 'automattic/wcpay/payment-request', + 'wcpay.express-checkout.cart-add-item', + 'automattic/wcpay/express-checkout', ( productData ) => { const $variationsForm = jQuery( '.variations_form' ); if ( ! $variationsForm.length ) { diff --git a/client/tokenized-payment-request/debounce.js b/client/tokenized-express-checkout/debounce.js similarity index 100% rename from client/tokenized-payment-request/debounce.js rename to client/tokenized-express-checkout/debounce.js diff --git a/client/tokenized-express-checkout/event-handlers.js b/client/tokenized-express-checkout/event-handlers.js index c2d3ad557ef..cc300e36ef5 100644 --- a/client/tokenized-express-checkout/event-handlers.js +++ b/client/tokenized-express-checkout/event-handlers.js @@ -118,7 +118,7 @@ export const onConfirmHandler = async ( paymentMethod.id ), extensions: applyFilters( - 'wcpay.payment-request.cart-place-order-extension-data', + 'wcpay.express-checkout.cart-place-order-extension-data', {} ), } ); diff --git a/client/tokenized-express-checkout/index.js b/client/tokenized-express-checkout/index.js index d18cb9f5be5..eca8a2f8a36 100644 --- a/client/tokenized-express-checkout/index.js +++ b/client/tokenized-express-checkout/index.js @@ -1,6 +1,9 @@ /* global jQuery, wcpayExpressCheckoutParams */ +/** + * External dependencies + */ import { __ } from '@wordpress/i18n'; -import { debounce } from 'lodash'; +import { addAction, removeAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -9,7 +12,7 @@ import WCPayAPI from '../checkout/api'; import '../checkout/express-checkout-buttons.scss'; import './compatibility/wc-deposits'; import './compatibility/wc-order-attribution'; -import './compatibility/wc-product-variations'; +import './compatibility/wc-product-page'; import { getExpressCheckoutButtonAppearance, getExpressCheckoutButtonStyleSettings, @@ -29,13 +32,66 @@ import { getCartApiHandler, } from './event-handlers'; import ExpressCheckoutOrderApi from './order-api'; +import ExpressCheckoutCartApi from './cart-api'; import { getUPEConfig } from 'wcpay/utils/checkout'; import expressCheckoutButtonUi from './button-ui'; import { transformCartDataForDisplayItems, transformCartDataForShippingRates, transformPrice, -} from 'wcpay/tokenized-express-checkout/transformers/wc-to-stripe'; +} from './transformers/wc-to-stripe'; + +let cachedCartData = null; +const noop = () => null; +const fetchNewCartData = async () => { + if ( getExpressCheckoutData( 'button_context' ) !== 'product' ) { + return await getCartApiHandler().getCart(); + } + + // creating a new cart and clearing it afterward, + // to avoid scenarios where the stock for a product with limited (or low) availability is added to the cart, + // preventing other customers from purchasing. + const temporaryCart = new ExpressCheckoutCartApi(); + temporaryCart.useSeparateCart(); + + const cartData = await temporaryCart.addProductToCart(); + + // no need to wait for the request to end, it can be done asynchronously. + // using `.finally( noop )` to avoid annoying IDE warnings. + temporaryCart.emptyCart().finally( noop ); + + return cartData; +}; + +const getServerSideExpressCheckoutProductData = () => { + const requestShipping = + getExpressCheckoutData( 'product' )?.needs_shipping ?? false; + const displayItems = ( + getExpressCheckoutData( 'product' )?.displayItems ?? [] + ).map( ( { label, amount } ) => ( { + name: label, + amount, + } ) ); + const shippingRates = requestShipping + ? [ + { + id: 'pending', + displayName: __( 'Pending', 'woocommerce-payments' ), + amount: 0, + }, + ] + : undefined; + + return { + total: getExpressCheckoutData( 'product' )?.total.amount, + currency: getExpressCheckoutData( 'product' )?.currency, + requestShipping, + shippingRates, + requestPhone: + getExpressCheckoutData( 'checkout' )?.needs_payer_phone ?? false, + displayItems, + }; +}; jQuery( ( $ ) => { // Don't load if blocks checkout is being loaded. @@ -47,7 +103,6 @@ jQuery( ( $ ) => { } const publishableKey = getExpressCheckoutData( 'stripe' ).publishableKey; - const quantityInputSelector = '.quantity .qty[type=number]'; if ( ! publishableKey ) { // If no configuration is present, probably this is not the checkout page. @@ -83,43 +138,10 @@ jQuery( ( $ ) => { $separator: jQuery( '#wcpay-express-checkout-button-separator' ), } ); - let wcPayECEError = ''; - const defaultErrorMessage = __( - 'There was an error getting the product information.', - 'woocommerce-payments' - ); - /** * Object to handle Stripe payment forms. */ const wcpayECE = { - getAttributes: function () { - const select = $( '.variations_form' ).find( '.variations select' ); - const data = {}; - let count = 0; - let chosen = 0; - - select.each( function () { - const attributeName = - $( this ).data( 'attribute_name' ) || - $( this ).attr( 'name' ); - const value = $( this ).val() || ''; - - if ( value.length > 0 ) { - chosen++; - } - - count++; - data[ attributeName ] = value; - } ); - - return { - count: count, - chosenCount: chosen, - data: data, - }; - }, - /** * Abort the payment and display error messages. * @@ -160,57 +182,6 @@ jQuery( ( $ ) => { window.location = url; }, - /** - * Adds the item to the cart and return cart details. - * - * @return {Promise} Promise for the request to the server. - */ - addToCart: () => { - let productId = $( '.single_add_to_cart_button' ).val(); - - // Check if product is a variable product. - if ( $( '.single_variation_wrap' ).length ) { - productId = $( '.single_variation_wrap' ) - .find( 'input[name="product_id"]' ) - .val(); - } - - if ( $( '.wc-bookings-booking-form' ).length ) { - productId = $( '.wc-booking-product-id' ).val(); - } - - const data = { - product_id: productId, - qty: $( quantityInputSelector ).val(), - attributes: $( '.variations_form' ).length - ? wcpayECE.getAttributes().data - : [], - }; - - // Add extension data to the POST body - const formData = $( 'form.cart' ).serializeArray(); - $.each( formData, ( i, field ) => { - if ( /^(addon-|wc_)/.test( field.name ) ) { - if ( /\[\]$/.test( field.name ) ) { - const fieldName = field.name.substring( - 0, - field.name.length - 2 - ); - if ( data[ fieldName ] ) { - data[ fieldName ].push( field.value ); - } else { - data[ fieldName ] = [ field.value ]; - } - } else { - data[ field.name ] = field.value; - } - } - } ); - - // TODO ~FR: replace with cartApi - return api.expressCheckoutECEAddToCart( data ); - }, - /** * Starts the Express Checkout Element * @@ -219,7 +190,7 @@ jQuery( ( $ ) => { startExpressCheckoutElement: async ( options ) => { const stripe = await api.getStripe(); const elements = stripe.elements( { - mode: options.mode ?? 'payment', + mode: 'payment', amount: options.total, currency: options.currency, paymentMethodCreation: 'manual', @@ -276,17 +247,16 @@ jQuery( ( $ ) => { return; } - if ( wcPayECEError ) { - window.alert( wcPayECEError ); - return; - } - // Add products to the cart if everything is right. - // TODO ~FR: use cartApi - wcpayECE.addToCart(); + getCartApiHandler().addProductToCart(); } const clickOptions = { + // `options.displayItems`, `options.requestShipping`, `options.requestPhone`, `options.shippingRates`, + // are all coming from prior of the initialization. + // The "real" values will be updated once the button loads. + // They are preemptively initialized because the `event.resolve({})` + // needs to be called within 1 second of the `click` event. lineItems: options.displayItems, emailRequired: true, shippingAddressRequired: options.requestShipping, @@ -322,6 +292,15 @@ jQuery( ( $ ) => { eceButton.on( 'cancel', async () => { wcpayECE.paymentAborted = true; + + if ( + getExpressCheckoutData( 'button_context' ) === 'product' + ) { + // clearing the cart to avoid issues with products with low or limited availability + // being held hostage by customers cancelling the ECE. + getCartApiHandler().emptyCart(); + } + onCancelHandler(); } ); @@ -339,224 +318,136 @@ jQuery( ( $ ) => { } } ); - if ( getExpressCheckoutData( 'button_context' ) === 'product' ) { - wcpayECE.attachProductPageEventListeners( elements ); - } - }, - - getSelectedProductData: () => { - let productId = $( '.single_add_to_cart_button' ).val(); - - // Check if product is a variable product. - if ( $( '.single_variation_wrap' ).length ) { - productId = $( '.single_variation_wrap' ) - .find( 'input[name="product_id"]' ) - .val(); - } - - if ( $( '.wc-bookings-booking-form' ).length ) { - productId = $( '.wc-booking-product-id' ).val(); - } - - const addons = - $( '#product-addons-total' ).data( 'price_data' ) || []; - const addonValue = addons.reduce( - ( sum, addon ) => sum + addon.cost, - 0 + removeAction( + 'wcpay.express-checkout.update-button-data', + 'automattic/wcpay/express-checkout' ); + addAction( + 'wcpay.express-checkout.update-button-data', + 'automattic/wcpay/express-checkout', + async () => { + try { + expressCheckoutButtonUi.blockButton(); - // WC Deposits Support. - const depositObject = {}; - if ( $( 'input[name=wc_deposit_option]' ).length ) { - depositObject.wc_deposit_option = $( - 'input[name=wc_deposit_option]:checked' - ).val(); - } - if ( $( 'input[name=wc_deposit_payment_plan]' ).length ) { - depositObject.wc_deposit_payment_plan = $( - 'input[name=wc_deposit_payment_plan]:checked' - ).val(); - } + cachedCartData = await fetchNewCartData(); + // checking if items needed shipping, before assigning new cart data. + const didItemsNeedShipping = options.requestShipping; - const data = { - product_id: productId, - qty: $( quantityInputSelector ).val(), - attributes: $( '.variations_form' ).length - ? wcpayECE.getAttributes().data - : [], - addon_value: addonValue, - ...depositObject, - }; - - // TODO ~FR: replace with cartApi - return api.expressCheckoutECEGetSelectedProductData( data ); - }, - - attachProductPageEventListeners: ( elements ) => { - // WooCommerce Deposits support. - // Trigger the "woocommerce_variation_has_changed" event when the deposit option is changed. - // Needs to be defined before the `woocommerce_variation_has_changed` event handler is set. - $( - 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' - ) - .off( 'change' ) - .on( 'change', () => { - $( 'form' ) - .has( - 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' - ) - .trigger( 'woocommerce_variation_has_changed' ); - } ); - - $( document.body ) - .off( 'woocommerce_variation_has_changed' ) - .on( 'woocommerce_variation_has_changed', () => { - expressCheckoutButtonUi.blockButton(); - - $.when( wcpayECE.getSelectedProductData() ) - .then( ( response ) => { - // TODO ~FR: this seems new - const isDeposits = wcpayECE.productHasDepositOption(); - /** - * If the customer aborted the express checkout, - * we need to re init the express checkout button to ensure the shipping - * options are refetched. If the customer didn't abort the express checkout, - * and the product's shipping status is consistent, - * we can simply update the express checkout button with the new total and display items. - */ - const needsShipping = - ! wcpayECE.paymentAborted && - getExpressCheckoutData( 'product' ) - .needs_shipping === response.needs_shipping; - - if ( ! isDeposits && needsShipping ) { - elements.update( { - amount: response.total.amount, - } ); - } else { - wcpayECE.reInitExpressCheckoutElement( - response - ); - } - } ) - .catch( () => { - expressCheckoutButtonUi.hideContainer(); - expressCheckoutButtonUi.getButtonSeparator().hide(); - } ) - .always( () => { - expressCheckoutButtonUi.unblockButton(); - } ); - } ); - - $( '.quantity' ) - .off( 'input', '.qty' ) - .on( - 'input', - '.qty', - debounce( () => { - expressCheckoutButtonUi.blockButton(); - wcPayECEError = ''; - - $.when( wcpayECE.getSelectedProductData() ) - .then( - ( response ) => { - // In case the server returns an unexpected response - if ( typeof response !== 'object' ) { - wcPayECEError = defaultErrorMessage; - } - - if ( - ! wcpayECE.paymentAborted && - getExpressCheckoutData( 'product' ) - .needs_shipping === - response.needs_shipping - ) { - elements.update( { - amount: response.total.amount, - } ); - } else { - wcpayECE.reInitExpressCheckoutElement( - response - ); - } + /** + * If the customer aborted the payment request, we need to re init the payment request button to ensure the shipping + * options are re-fetched. If the customer didn't abort the payment request, and the product's shipping status is + * consistent, we can simply update the payment request button with the new total and display items. + */ + if ( + ! wcpayECE.paymentAborted && + didItemsNeedShipping === + cachedCartData.needs_shipping + ) { + elements.update( { + total: { + label: getExpressCheckoutData( + 'total_label' + ), + amount: transformPrice( + parseInt( + cachedCartData.totals.total_price, + 10 + ) - + parseInt( + cachedCartData.totals + .total_refund || 0, + 10 + ), + cachedCartData.totals + ), }, - ( response ) => { - wcPayECEError = - response.responseJSON?.error ?? - defaultErrorMessage; - } - ) - .always( function () { - expressCheckoutButtonUi.unblockButton(); + displayItems: transformCartDataForDisplayItems( + cachedCartData + ), } ); - }, 250 ) - ); - }, + } else { + // the cachedCartData from the Store API will be used from now on, + // instead of the `product` attributes. + wcpayExpressCheckoutParams.product = null; - reInitExpressCheckoutElement: ( response ) => { - wcpayExpressCheckoutParams.product.needs_shipping = - response.needs_shipping; - wcpayExpressCheckoutParams.product.total = response.total; - wcpayExpressCheckoutParams.product.displayItems = - response.displayItems; - wcpayECE.init(); - }, + await wcpayECE.init(); + } - productHasDepositOption() { - return !! $( 'form' ).has( - 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' - ).length; + expressCheckoutButtonUi.unblockButton(); + } catch ( e ) { + expressCheckoutButtonUi.hide(); + } + } + ); }, /** * Initialize event handlers and UI state */ init: async () => { + // on product pages, we should be able to have `getExpressCheckoutData( 'product' )` from the backend, + // which saves us some AJAX calls. + if ( ! getExpressCheckoutData( 'product' ) && ! cachedCartData ) { + try { + cachedCartData = await fetchNewCartData(); + } catch ( e ) { + // if something fails here, we can likely fall back on `getExpressCheckoutData( 'product' )`. + } + } + + // once (and if) cart data has been fetched, we can safely clear product data from the backend. + if ( cachedCartData ) { + wcpayExpressCheckoutParams.product = undefined; + } + if ( getExpressCheckoutData( 'button_context' ) === 'product' ) { - await wcpayECE.startExpressCheckoutElement( { - mode: 'payment', - total: getExpressCheckoutData( 'product' )?.total.amount, - currency: getExpressCheckoutData( 'product' )?.currency, - requestShipping: - getExpressCheckoutData( 'product' )?.needs_shipping ?? - false, - requestPhone: - getExpressCheckoutData( 'checkout' ) - ?.needs_payer_phone ?? false, - displayItems: getExpressCheckoutData( 'product' ) - .displayItems, - } ); - } else { + // on product pages, we need to interact with an anonymous cart to check out the product, + // so that we don't affect the products in the main cart. + // On cart, checkout, place order pages we instead use the cart itself. + getCartApiHandler().useSeparateCart(); + } + + if ( cachedCartData ) { // If this is the cart page, or checkout page, or pay-for-order page, we need to request the cart details. - const cartData = await getCartApiHandler().getCart(); + // but if the data is not available, we can't render the button. const total = transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( cartData.totals.total_refund || 0, 10 ), - cartData.totals + parseInt( cachedCartData.totals.total_price, 10 ) - + parseInt( cachedCartData.totals.total_refund || 0, 10 ), + cachedCartData.totals ); if ( total === 0 ) { expressCheckoutButtonUi.hideContainer(); expressCheckoutButtonUi.getButtonSeparator().hide(); } else { await wcpayECE.startExpressCheckoutElement( { - mode: 'payment', total, - currency: cartData.totals.currency_code.toLowerCase(), + currency: cachedCartData.totals.currency_code.toLowerCase(), // pay-for-order should never display the shipping selection. requestShipping: getExpressCheckoutData( 'button_context' ) !== - 'pay_for_order' && cartData.needs_shipping, + 'pay_for_order' && + cachedCartData.needs_shipping, shippingRates: transformCartDataForShippingRates( - cartData + cachedCartData ), requestPhone: getExpressCheckoutData( 'checkout' ) ?.needs_payer_phone ?? false, displayItems: transformCartDataForDisplayItems( - cartData + cachedCartData ), } ); } + } else if ( + getExpressCheckoutData( 'button_context' ) === 'product' && + getExpressCheckoutData( 'product' ) + ) { + await wcpayECE.startExpressCheckoutElement( + getServerSideExpressCheckoutProductData() + ); + } else { + expressCheckoutButtonUi.hideContainer(); + expressCheckoutButtonUi.getButtonSeparator().hide(); } // After initializing a new express checkout button, we need to reset the paymentAborted flag. From 02da62a50aa12d5ab10da027f4ece009069e37f5 Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto Date: Thu, 12 Dec 2024 11:47:33 -0300 Subject: [PATCH 12/26] Fixed UPE country detection in Checkout for non-logged in users (#9912) Co-authored-by: Timur Karimov --- changelog/fix-upe-country-selection | 4 ++++ client/checkout/utils/upe.js | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-upe-country-selection diff --git a/changelog/fix-upe-country-selection b/changelog/fix-upe-country-selection new file mode 100644 index 00000000000..478ffa1cfcd --- /dev/null +++ b/changelog/fix-upe-country-selection @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed UPE country detection in Checkout for non-logged in users diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js index 500314b9f5b..af8e0427c04 100644 --- a/client/checkout/utils/upe.js +++ b/client/checkout/utils/upe.js @@ -375,10 +375,9 @@ export const togglePaymentMethodForCountry = ( upeElement ) => { billingInput = document.querySelector( '#billing_country' ); } - /* global wcpayCustomerData */ // in the case of "pay for order", there is no "billing country" input, so we need to rely on backend data. const billingCountry = - billingInput?.value || wcpayCustomerData?.billing_country || ''; + billingInput?.value || window?.wcpayCustomerData?.billing_country || ''; const upeContainer = upeElement?.closest( '.wc_payment_method' ); if ( supportedCountries.includes( billingCountry ) ) { From 52dbe03182d6a6fad5486ba2e6041a3043c5847c Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:11:41 -0600 Subject: [PATCH 13/26] Pass footer header styles to WooPay (#9880) --- .../add-pass-footer-header-styles-to-woopay | 5 ++++ client/checkout/api/test/index.test.js | 3 +++ client/checkout/upe-styles/index.js | 12 ++++++++++ client/checkout/upe-styles/test/index.js | 23 +++++++++++++++++++ client/checkout/upe-styles/upe-styles.js | 13 +++++++++++ 5 files changed, 56 insertions(+) create mode 100644 changelog/add-pass-footer-header-styles-to-woopay diff --git a/changelog/add-pass-footer-header-styles-to-woopay b/changelog/add-pass-footer-header-styles-to-woopay new file mode 100644 index 00000000000..ab6375db250 --- /dev/null +++ b/changelog/add-pass-footer-header-styles-to-woopay @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Impovements to WooPay themeing, which is not yet released to the public. + + diff --git a/client/checkout/api/test/index.test.js b/client/checkout/api/test/index.test.js index 8ec819ea4c0..7fdd80e9b3a 100644 --- a/client/checkout/api/test/index.test.js +++ b/client/checkout/api/test/index.test.js @@ -43,6 +43,9 @@ const mockAppearance = { '.Button': {}, '.Link': {}, '.Container': {}, + '.Footer': {}, + '.Footer-link': {}, + '.Header': {}, }, theme: 'stripe', variables: { diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js index 5c775caf43e..45c9ea83578 100644 --- a/client/checkout/upe-styles/index.js +++ b/client/checkout/upe-styles/index.js @@ -156,6 +156,9 @@ export const appearanceSelectors = { buttonSelectors: [ '#place_order' ], linkSelectors: [ 'a' ], containerSelectors: [ '.woocommerce-checkout-review-order-table' ], + headerSelectors: [ '.site-header' ], + footerSelectors: [ '.site-footer' ], + footerLink: [ '.site-footer a' ], }, /** @@ -514,6 +517,12 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { selectors.containerSelectors, '.Container' ); + const headerRules = getFieldStyles( selectors.headerSelectors, '.Header' ); + const footerRules = getFieldStyles( selectors.footerSelectors, '.Footer' ); + const footerLinkRules = getFieldStyles( + selectors.footerLink, + '.Footer--link' + ); const globalRules = { colorBackground: backgroundColor, colorText: paragraphRules.color, @@ -559,6 +568,9 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { appearance.rules = { ...appearance.rules, '.Heading': headingRules, + '.Header': headerRules, + '.Footer': footerRules, + '.Footer-link': footerLinkRules, '.Button': buttonRules, '.Link': linkRules, '.Container': containerRules, diff --git a/client/checkout/upe-styles/test/index.js b/client/checkout/upe-styles/test/index.js index 96a602a4bbd..22fd181df90 100644 --- a/client/checkout/upe-styles/test/index.js +++ b/client/checkout/upe-styles/test/index.js @@ -225,6 +225,29 @@ describe( 'Getting styles for automated theming', () => { '.Container': { backgroundColor: 'rgba(0, 0, 0, 0)', }, + '.Footer': { + color: 'rgb(109, 109, 109)', + backgroundColor: 'rgba(0, 0, 0, 0)', + fontFamily: + '"Source Sans Pro", HelveticaNeue-Light, "Helvetica Neue Light"', + fontSize: '12px', + padding: '10px', + }, + '.Footer-link': { + color: 'rgb(109, 109, 109)', + fontFamily: + '"Source Sans Pro", HelveticaNeue-Light, "Helvetica Neue Light"', + fontSize: '12px', + padding: '10px', + }, + '.Header': { + color: 'rgb(109, 109, 109)', + backgroundColor: 'rgba(0, 0, 0, 0)', + fontFamily: + '"Source Sans Pro", HelveticaNeue-Light, "Helvetica Neue Light"', + fontSize: '12px', + padding: '10px', + }, }, labels: 'above', } ); diff --git a/client/checkout/upe-styles/upe-styles.js b/client/checkout/upe-styles/upe-styles.js index b578960317e..72903e459d7 100644 --- a/client/checkout/upe-styles/upe-styles.js +++ b/client/checkout/upe-styles/upe-styles.js @@ -78,6 +78,16 @@ const upeSupportedProperties = { ...borderOutlineBackgroundProps.slice( 1 ), // Remove backgroundColor ], '.Container': [ ...borderOutlineBackgroundProps ], + '.Header': [ + ...paddingColorProps, + ...borderOutlineBackgroundProps, + ...textFontTransitionProps, + ], + '.Footer': [ + ...paddingColorProps, + ...borderOutlineBackgroundProps, + ...textFontTransitionProps, + ], }; // Restricted properties allowed to generate the automated theming of UPE. @@ -113,6 +123,9 @@ export const upeRestrictedProperties = { '.TabLabel': upeSupportedProperties[ '.TabLabel' ], '.Block': upeSupportedProperties[ '.Block' ], '.Container': upeSupportedProperties[ '.Container' ], + '.Header': upeSupportedProperties[ '.Header' ], + '.Footer': upeSupportedProperties[ '.Footer' ], + '.Footer--link': upeSupportedProperties[ '.Text' ], '.Text': upeSupportedProperties[ '.Text' ], '.Text--redirect': upeSupportedProperties[ '.Text' ], }; From 9729695739be9e4b491ec91c454801c7f3dfda9e Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 12 Dec 2024 17:48:25 +0100 Subject: [PATCH 14/26] chore: remove tokeinzed payment request code (#9920) --- .eslintignore | 4 - ...emove-tokenized-payment-request-references | 5 + client/tokenized-payment-request/README.md | 4 - .../blocks/apple-pay-preview.js | 3 - .../tokenized-payment-request/blocks/index.js | 62 -- .../blocks/payment-request-express.js | 163 ---- .../blocks/use-initialization.js | 172 ---- client/tokenized-payment-request/button-ui.js | 47 - .../event-handlers.js | 178 ---- .../frontend-utils.js | 257 ------ client/tokenized-payment-request/index.js | 90 -- .../payment-request.js | 478 ---------- .../test/payment-request.test.js | 158 ---- client/tokenized-payment-request/tracking.js | 36 - .../transformers/stripe-to-wc.js | 103 --- ...ayments-payment-request-button-handler.php | 873 ------------------ psalm-baseline.xml | 7 - tests/js/jest.config.js | 2 - tests/unit/bootstrap.php | 2 - ...ayments-payment-request-button-handler.php | 650 ------------- 20 files changed, 5 insertions(+), 3289 deletions(-) create mode 100644 changelog/chore-remove-tokenized-payment-request-references delete mode 100644 client/tokenized-payment-request/README.md delete mode 100644 client/tokenized-payment-request/blocks/apple-pay-preview.js delete mode 100644 client/tokenized-payment-request/blocks/index.js delete mode 100644 client/tokenized-payment-request/blocks/payment-request-express.js delete mode 100644 client/tokenized-payment-request/blocks/use-initialization.js delete mode 100644 client/tokenized-payment-request/button-ui.js delete mode 100644 client/tokenized-payment-request/event-handlers.js delete mode 100644 client/tokenized-payment-request/frontend-utils.js delete mode 100644 client/tokenized-payment-request/index.js delete mode 100644 client/tokenized-payment-request/payment-request.js delete mode 100644 client/tokenized-payment-request/test/payment-request.test.js delete mode 100644 client/tokenized-payment-request/tracking.js delete mode 100644 client/tokenized-payment-request/transformers/stripe-to-wc.js delete mode 100644 includes/class-wc-payments-payment-request-button-handler.php delete mode 100644 tests/unit/test-class-wc-payments-payment-request-button-handler.php diff --git a/.eslintignore b/.eslintignore index e558812a35a..590187b9d5e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,7 +10,3 @@ vendor/* release/* tests/e2e/docker* tests/e2e/deps* - -# We'll delete the directory and its contents as part of https://github.com/Automattic/woocommerce-payments/issues/9722 . -# ignoring it because we're temporariily cleaning it up. -client/tokenized-payment-request diff --git a/changelog/chore-remove-tokenized-payment-request-references b/changelog/chore-remove-tokenized-payment-request-references new file mode 100644 index 00000000000..56dc3b0a0cc --- /dev/null +++ b/changelog/chore-remove-tokenized-payment-request-references @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: chore: remove tokeinzed payment request code + + diff --git a/client/tokenized-payment-request/README.md b/client/tokenized-payment-request/README.md deleted file mode 100644 index 2c92ba8d2ff..00000000000 --- a/client/tokenized-payment-request/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Tokenized Payment Request Button - -This directory contains the JS work done by the Heisenberg team to convert the PRBs to leverage the Store API. -We'll delete the directory and its contents as part of https://github.com/Automattic/woocommerce-payments/issues/9722 . diff --git a/client/tokenized-payment-request/blocks/apple-pay-preview.js b/client/tokenized-payment-request/blocks/apple-pay-preview.js deleted file mode 100644 index 6b6f543ed05..00000000000 --- a/client/tokenized-payment-request/blocks/apple-pay-preview.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable max-len */ -export const applePayImage = - "data:image/svg+xml,%3Csvg width='264' height='48' viewBox='0 0 264 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='264' height='48' rx='3' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.114 16.6407C125.682 15.93 126.067 14.9756 125.966 14C125.135 14.0415 124.121 14.549 123.533 15.2602C123.006 15.8693 122.539 16.8641 122.661 17.7983C123.594 17.8797 124.526 17.3317 125.114 16.6407Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.955 17.982C124.601 17.9011 123.448 18.7518 122.801 18.7518C122.154 18.7518 121.163 18.0224 120.092 18.0421C118.696 18.0629 117.402 18.8524 116.694 20.1079C115.238 22.6196 116.31 26.3453 117.726 28.3909C118.414 29.4028 119.242 30.5174 120.334 30.4769C121.366 30.4365 121.77 29.8087 123.024 29.8087C124.277 29.8087 124.641 30.4769 125.733 30.4567C126.865 30.4365 127.573 29.4443 128.261 28.4313C129.049 27.2779 129.373 26.1639 129.393 26.1027C129.373 26.0825 127.209 25.2515 127.189 22.7606C127.169 20.6751 128.888 19.6834 128.969 19.6217C127.998 18.1847 126.481 18.0224 125.955 17.982Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M136.131 23.1804H138.834C140.886 23.1804 142.053 22.0752 142.053 20.1592C142.053 18.2432 140.886 17.1478 138.845 17.1478H136.131V23.1804ZM139.466 15.1582C142.411 15.1582 144.461 17.1903 144.461 20.1483C144.461 23.1172 142.369 25.1596 139.392 25.1596H136.131V30.3498H133.775V15.1582H139.466Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M152.198 26.224V25.3712L149.579 25.5397C148.106 25.6341 147.339 26.182 147.339 27.14C147.339 28.0664 148.138 28.6667 149.39 28.6667C150.988 28.6667 152.198 27.6449 152.198 26.224ZM145.046 27.2032C145.046 25.2551 146.529 24.1395 149.263 23.971L152.198 23.7922V22.9498C152.198 21.7181 151.388 21.0442 149.947 21.0442C148.758 21.0442 147.896 21.6548 147.717 22.5916H145.592C145.656 20.6232 147.507 19.1914 150.01 19.1914C152.703 19.1914 154.459 20.602 154.459 22.7917V30.351H152.282V28.5298H152.229C151.609 29.719 150.241 30.4666 148.758 30.4666C146.571 30.4666 145.046 29.1612 145.046 27.2032Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.461 34.4145V32.5934C156.608 32.6141 156.965 32.6354 157.155 32.6354C158.196 32.6354 158.785 32.1932 159.142 31.0564L159.353 30.3824L155.366 19.3281H157.827L160.604 28.298H160.657L163.434 19.3281H165.832L161.698 30.9402C160.752 33.6038 159.668 34.4778 157.376 34.4778C157.197 34.4778 156.618 34.4565 156.461 34.4145Z' fill='white'/%3E%3C/svg%3E%0A"; diff --git a/client/tokenized-payment-request/blocks/index.js b/client/tokenized-payment-request/blocks/index.js deleted file mode 100644 index f6cb3461102..00000000000 --- a/client/tokenized-payment-request/blocks/index.js +++ /dev/null @@ -1,62 +0,0 @@ -/* global wcpayConfig, wcpayPaymentRequestParams */ - -/** - * Internal dependencies - */ -import { PaymentRequestExpress } from './payment-request-express'; -import { applePayImage } from './apple-pay-preview'; -import { getConfig } from '../../utils/checkout'; -import { - getPaymentRequest, - transformCartDataForStoreAPI, -} from '../frontend-utils'; - -const PAYMENT_METHOD_NAME_PAYMENT_REQUEST = - 'woocommerce_payments_tokenized_cart_payment_request'; - -const ApplePayPreview = () => ; - -const tokenizedCartPaymentRequestPaymentMethod = ( api ) => ( { - name: PAYMENT_METHOD_NAME_PAYMENT_REQUEST, - content: ( - - ), - edit: , - canMakePayment: ( cartData ) => { - // If in the editor context, always return true to display the `edit` prop preview. - // https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/4101. - if ( getConfig( 'is_admin' ) ) { - return true; - } - - if ( typeof wcpayPaymentRequestParams === 'undefined' ) { - return false; - } - - if ( typeof wcpayConfig !== 'undefined' ) { - return false; - } - - return api.loadStripeForExpressCheckout().then( ( stripe ) => { - // Create a payment request and check if we can make a payment to determine whether to - // show the Payment Request Button or not. This is necessary because a browser might be - // able to load the Stripe JS object, but not support Payment Requests. - cartData = transformCartDataForStoreAPI( cartData, null ); - const pr = getPaymentRequest( { - stripe, - cartData, - } ); - - return pr.canMakePayment(); - } ); - }, - paymentMethodId: PAYMENT_METHOD_NAME_PAYMENT_REQUEST, - supports: { - features: getConfig( 'features' ), - }, -} ); - -export default tokenizedCartPaymentRequestPaymentMethod; diff --git a/client/tokenized-payment-request/blocks/payment-request-express.js b/client/tokenized-payment-request/blocks/payment-request-express.js deleted file mode 100644 index 3e852ae8486..00000000000 --- a/client/tokenized-payment-request/blocks/payment-request-express.js +++ /dev/null @@ -1,163 +0,0 @@ -/* global wcpayPaymentRequestParams */ - -/** - * External dependencies - */ -import { Elements, PaymentRequestButtonElement } from '@stripe/react-stripe-js'; -import { recordUserEvent } from 'tracks'; -import { useEffect, useState } from 'react'; - -/** - * Internal dependencies - */ -import { useInitialization } from './use-initialization'; -import { getPaymentRequestData } from '../frontend-utils'; - -/** - * PaymentRequestExpressComponent - * - * @param {Object} props Incoming props. - * - * @return {ReactNode} Payment Request button component. - */ -const PaymentRequestExpressComponent = ( { - api, - billing, - shippingData, - setExpressPaymentError, - onClick, - onClose, - onPaymentRequestAvailable, - cartData, -} ) => { - // TODO: Don't display custom button when result.requestType - // is `apple_pay` or `google_pay`. - const { - paymentRequest, - // paymentRequestType, - onButtonClick, - } = useInitialization( { - api, - billing, - shippingData, - setExpressPaymentError, - onClick, - onClose, - cartData, - } ); - - useEffect( () => { - if ( paymentRequest ) { - const orderAttribution = window?.wc_order_attribution; - if ( orderAttribution ) { - orderAttribution.setOrderTracking( - orderAttribution.params.allowTracking - ); - } - } - }, [ paymentRequest ] ); - - const { type, theme, height } = getPaymentRequestData( 'button' ); - - const paymentRequestButtonStyle = { - paymentRequestButton: { - type, - theme, - height: height + 'px', - }, - }; - - if ( ! paymentRequest ) { - return null; - } - - let paymentRequestType = ''; - - // Check the availability of the Payment Request API first. - paymentRequest.canMakePayment().then( ( result ) => { - if ( ! result ) { - return; - } - - // Set the payment request type. - if ( result.applePay ) { - paymentRequestType = 'apple_pay'; - } else if ( result.googlePay ) { - paymentRequestType = 'google_pay'; - } - onPaymentRequestAvailable( paymentRequestType ); - } ); - - const onPaymentRequestButtonClick = ( event ) => { - onButtonClick( event, paymentRequest ); - - const paymentRequestTypeEvents = { - google_pay: 'gpay_button_click', - apple_pay: 'applepay_button_click', - }; - - if ( paymentRequestTypeEvents.hasOwnProperty( paymentRequestType ) ) { - const paymentRequestEvent = - paymentRequestTypeEvents[ paymentRequestType ]; - recordUserEvent( paymentRequestEvent, { - source: wcpayPaymentRequestParams?.button_context, - } ); - } - }; - - return ( -
- - -
- ); -}; - -/** - * PaymentRequestExpress express payment method component. - * - * @param {Object} props PaymentMethodProps. - * - * @return {ReactNode} Stripe Elements component. - */ -export const PaymentRequestExpress = ( props ) => { - const { stripe } = props; - const [ paymentRequestType, setPaymentRequestType ] = useState( false ); - - const handlePaymentRequestAvailability = ( paymentType ) => { - setPaymentRequestType( paymentType ); - }; - - useEffect( () => { - if ( paymentRequestType ) { - const paymentRequestTypeEvents = { - google_pay: 'gpay_button_load', - apple_pay: 'applepay_button_load', - }; - - if ( - paymentRequestTypeEvents.hasOwnProperty( paymentRequestType ) - ) { - const event = paymentRequestTypeEvents[ paymentRequestType ]; - recordUserEvent( event, { - source: wcpayPaymentRequestParams?.button_context, - } ); - } - } - }, [ paymentRequestType ] ); - - return ( - - - - ); -}; diff --git a/client/tokenized-payment-request/blocks/use-initialization.js b/client/tokenized-payment-request/blocks/use-initialization.js deleted file mode 100644 index 360e2836eeb..00000000000 --- a/client/tokenized-payment-request/blocks/use-initialization.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useState, useCallback } from '@wordpress/element'; -import { useStripe } from '@stripe/react-stripe-js'; - -/** - * Internal dependencies - */ -import { - shippingAddressChangeHandler, - shippingOptionChangeHandler, - paymentMethodHandler, -} from '../event-handlers.js'; - -import { - getPaymentRequest, - getPaymentRequestData, - transformCartDataForStoreAPI, - updatePaymentRequest, - displayLoginConfirmationDialog, -} from '../frontend-utils.js'; - -export const useInitialization = ( { - api, - billing, - shippingData, - setExpressPaymentError, - onClick, - onClose, - cartData, -} ) => { - cartData = transformCartDataForStoreAPI( null, { - ...cartData, - ...billing, - ...shippingData, - } ); - - const stripe = useStripe(); - - const [ paymentRequest, setPaymentRequest ] = useState( null ); - const [ isFinished, setIsFinished ] = useState( false ); - const [ paymentRequestType, setPaymentRequestType ] = useState( '' ); - - // Create the initial paymentRequest object. Note, we can't do anything if stripe isn't available yet or we have zero total. - useEffect( () => { - if ( - ! stripe || - ! billing?.cartTotal?.value || - isFinished || - paymentRequest - ) { - return; - } - - const pr = getPaymentRequest( { - stripe, - cartData, - } ); - - pr.canMakePayment().then( ( result ) => { - if ( result ) { - setPaymentRequest( pr ); - if ( result.applePay ) { - setPaymentRequestType( 'apple_pay' ); - } else if ( result.googlePay ) { - setPaymentRequestType( 'google_pay' ); - } else { - setPaymentRequestType( 'payment_request_api' ); - } - } - } ); - }, [ - stripe, - paymentRequest, - billing?.cartTotal?.value, - isFinished, - shippingData?.needsShipping, - billing?.cartTotalItems, - cartData, - ] ); - - // It's not possible to update the `requestShipping` property in the `paymentRequest` - // object, so when `needsShipping` changes, we need to reset the `paymentRequest` object. - useEffect( () => { - setPaymentRequest( null ); - }, [ shippingData.needsShipping ] ); - - // When the payment button is clicked, update the request and show it. - const onButtonClick = useCallback( - ( evt, pr ) => { - // If login is required, display redirect confirmation dialog. - if ( getPaymentRequestData( 'login_confirmation' ) ) { - evt.preventDefault(); - displayLoginConfirmationDialog( paymentRequestType ); - return; - } - - setIsFinished( false ); - setExpressPaymentError( '' ); - updatePaymentRequest( { - paymentRequest, - cartData, - } ); - onClick(); - - // We must manually call payment request `show()` for custom buttons. - if ( pr ) { - pr.show(); - } - }, - [ - setExpressPaymentError, - paymentRequest, - cartData, - onClick, - paymentRequestType, - ] - ); - - // Whenever paymentRequest changes, hook in event listeners. - useEffect( () => { - const cancelHandler = () => { - setIsFinished( false ); - setPaymentRequest( null ); - onClose(); - }; - - const completePayment = ( redirectUrl ) => { - setIsFinished( true ); - window.location = redirectUrl; - }; - - const abortPayment = ( paymentMethod, message ) => { - paymentMethod.complete( 'fail' ); - setIsFinished( true ); - setExpressPaymentError( message ); - }; - - paymentRequest?.on( 'shippingaddresschange', ( event ) => - shippingAddressChangeHandler( event ) - ); - - paymentRequest?.on( 'shippingoptionchange', ( event ) => - shippingOptionChangeHandler( event ) - ); - - paymentRequest?.on( 'paymentmethod', ( event ) => - paymentMethodHandler( api, completePayment, abortPayment, event ) - ); - - paymentRequest?.on( 'cancel', cancelHandler ); - - return () => { - paymentRequest?.removeAllListeners(); - }; - }, [ - setExpressPaymentError, - paymentRequest, - api, - setIsFinished, - setPaymentRequest, - onClose, - cartData, - ] ); - - return { - paymentRequest, - onButtonClick, - paymentRequestType, - }; -}; diff --git a/client/tokenized-payment-request/button-ui.js b/client/tokenized-payment-request/button-ui.js deleted file mode 100644 index b0ca818d213..00000000000 --- a/client/tokenized-payment-request/button-ui.js +++ /dev/null @@ -1,47 +0,0 @@ -/* global jQuery */ - -let $wcpayPaymentRequestContainer = null; - -const paymentRequestButtonUi = { - init: ( { $container } ) => { - $wcpayPaymentRequestContainer = $container; - }, - - getElements: () => { - return jQuery( - '.wcpay-express-checkout-wrapper,#wcpay-express-checkout-button-separator' - ); - }, - - blockButton: () => { - // check if element isn't already blocked before calling block() to avoid blinking overlay issues - // blockUI.isBlocked is either undefined or 0 when element is not blocked - if ( $wcpayPaymentRequestContainer.data( 'blockUI.isBlocked' ) ) { - return; - } - - $wcpayPaymentRequestContainer.block( { message: null } ); - }, - - unblockButton: () => { - paymentRequestButtonUi.show(); - $wcpayPaymentRequestContainer.unblock(); - }, - - showButton: ( paymentRequestButton ) => { - if ( $wcpayPaymentRequestContainer.length ) { - paymentRequestButtonUi.show(); - paymentRequestButton.mount( '#wcpay-payment-request-button' ); - } - }, - - hide: () => { - paymentRequestButtonUi.getElements().hide(); - }, - - show: () => { - paymentRequestButtonUi.getElements().show(); - }, -}; - -export default paymentRequestButtonUi; diff --git a/client/tokenized-payment-request/event-handlers.js b/client/tokenized-payment-request/event-handlers.js deleted file mode 100644 index 872f51c86b3..00000000000 --- a/client/tokenized-payment-request/event-handlers.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * External dependencies - */ -import { applyFilters } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { - transformStripePaymentMethodForStoreApi, - transformStripeShippingAddressForStoreApi, -} from './transformers/stripe-to-wc'; -import { - transformCartDataForDisplayItems, - transformCartDataForShippingOptions, - transformPrice, -} from './transformers/wc-to-stripe'; - -import { - getPaymentRequestData, - getErrorMessageFromNotice, -} from './frontend-utils'; - -import PaymentRequestCartApi from './cart-api'; - -const cartApi = new PaymentRequestCartApi(); - -export const shippingAddressChangeHandler = async ( event ) => { - try { - // Please note that the `event.shippingAddress` might not contain all the fields. - // Some fields might not be present (like `line_1` or `line_2`) due to semi-anonymized data. - const cartData = await cartApi.updateCustomer( - transformStripeShippingAddressForStoreApi( event.shippingAddress ) - ); - - const shippingOptions = transformCartDataForShippingOptions( cartData ); - - // when no shipping options are returned, the API still returns a 200 status code. - // We need to ensure that shipping options are present - otherwise the PRB dialog won't update correctly. - if ( shippingOptions.length === 0 ) { - event.updateWith( { - // Possible statuses: https://docs.stripe.com/js/appendix/payment_response#payment_response_object-complete - status: 'invalid_shipping_address', - } ); - - return; - } - - event.updateWith( { - // Possible statuses: https://docs.stripe.com/js/appendix/payment_response#payment_response_object-complete - status: 'success', - shippingOptions: transformCartDataForShippingOptions( cartData ), - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( cartData.totals.total_refund || 0, 10 ), - cartData.totals - ), - }, - displayItems: transformCartDataForDisplayItems( cartData ), - } ); - } catch ( error ) { - // Possible statuses: https://docs.stripe.com/js/appendix/payment_response#payment_response_object-complete - event.updateWith( { - status: 'fail', - } ); - } -}; - -export const shippingOptionChangeHandler = async ( event ) => { - try { - const cartData = await cartApi.selectShippingRate( { - package_id: 0, - rate_id: event.shippingOption.id, - } ); - - event.updateWith( { - status: 'success', - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( cartData.totals.total_refund || 0, 10 ), - cartData.totals - ), - }, - displayItems: transformCartDataForDisplayItems( cartData ), - } ); - } catch ( error ) { - event.updateWith( { status: 'fail' } ); - } -}; - -const paymentResponseHandler = async ( - api, - response, - completePayment, - abortPayment, - event -) => { - if ( response.payment_result.payment_status !== 'success' ) { - return abortPayment( - event, - getErrorMessageFromNotice( - response.message || - response.payment_result?.payment_details.find( - ( detail ) => detail.key === 'errorMessage' - )?.value - ) - ); - } - - try { - const confirmationRequest = api.confirmIntent( - response.payment_result.redirect_url - ); - // We need to call `complete` outside of `completePayment` to close the dialog for 3DS. - event.complete( 'success' ); - - // `true` means there is no intent to confirm. - if ( confirmationRequest === true ) { - completePayment( response.payment_result.redirect_url ); - } else { - const redirectUrl = await confirmationRequest; - - completePayment( redirectUrl ); - } - } catch ( error ) { - abortPayment( - event, - getErrorMessageFromNotice( - error.message || - error.payment_result?.payment_details.find( - ( detail ) => detail.key === 'errorMessage' - )?.value - ) - ); - } -}; - -export const paymentMethodHandler = async ( - api, - completePayment, - abortPayment, - event -) => { - try { - // Kick off checkout processing step. - const response = await cartApi.placeOrder( { - // adding extension data as a separate action, - // so that we make it harder for external plugins to modify or intercept checkout data. - ...transformStripePaymentMethodForStoreApi( event ), - extensions: applyFilters( - 'wcpay.payment-request.cart-place-order-extension-data', - {} - ), - } ); - - paymentResponseHandler( - api, - response, - completePayment, - abortPayment, - event - ); - } catch ( error ) { - abortPayment( - event, - getErrorMessageFromNotice( - error.message || - error.payment_result?.payment_details.find( - ( detail ) => detail.key === 'errorMessage' - )?.value - ) - ); - } -}; diff --git a/client/tokenized-payment-request/frontend-utils.js b/client/tokenized-payment-request/frontend-utils.js deleted file mode 100644 index c1dd1d4d46c..00000000000 --- a/client/tokenized-payment-request/frontend-utils.js +++ /dev/null @@ -1,257 +0,0 @@ -/* global wcpayPaymentRequestParams */ - -/** - * Internal dependencies - */ -import { - transformCartDataForDisplayItems, - transformPrice, -} from './transformers/wc-to-stripe'; - -/** - * Retrieves payment request data from global variable. - * - * @param {string} key The object property key. - * @return {mixed} Value of the object prop or null. - */ -export const getPaymentRequestData = ( key ) => { - if ( - typeof wcpayPaymentRequestParams === 'object' && - wcpayPaymentRequestParams.hasOwnProperty( key ) - ) { - return wcpayPaymentRequestParams[ key ]; - } - return null; -}; - -/** - * Returns a Stripe payment request object. - * - * @param {Object} config A configuration object for getting the payment request. - * @return {Object} Payment Request options object - */ -export const getPaymentRequest = ( { stripe, cartData, productData } ) => { - // the country code defined here comes from the WC settings. - // It might be interesting to ensure the country code coincides with the Stripe account's country, - // as defined here: https://docs.stripe.com/js/payment_request/create - let country = getPaymentRequestData( 'checkout' )?.country_code; - - // Puerto Rico (PR) is the only US territory/possession that's supported by Stripe. - // Since it's considered a US state by Stripe, we need to do some special mapping. - if ( country === 'PR' ) { - country = 'US'; - } - - return stripe.paymentRequest( { - country, - requestPayerName: true, - requestPayerEmail: true, - requestPayerPhone: getPaymentRequestData( 'checkout' ) - ?.needs_payer_phone, - ...( productData - ? { - // we can't just pass `productData`, and we need a little bit of massaging for older data. - currency: productData.currency, - total: productData.total, - displayItems: productData.displayItems, - requestShipping: productData.needs_shipping, - } - : { - currency: cartData.totals.currency_code.toLowerCase(), - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( - cartData.totals.total_refund || 0, - 10 - ), - cartData.totals - ), - }, - requestShipping: - getPaymentRequestData( 'button_context' ) === - 'pay_for_order' - ? false - : cartData.needs_shipping, - displayItems: transformCartDataForDisplayItems( cartData ), - } ), - } ); -}; - -/** - * Utility function for updating the Stripe PaymentRequest object - * - * @param {Object} update An object containing the things needed for the update. - */ -export const updatePaymentRequest = ( { paymentRequest, cartData } ) => { - paymentRequest.update( { - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( cartData.totals.total_refund || 0, 10 ), - cartData.totals - ), - }, - displayItems: transformCartDataForDisplayItems( cartData ), - } ); -}; - -/** - * Displays a `confirm` dialog which leads to a redirect. - * - * @param {string} paymentRequestType Can be either apple_pay, google_pay or payment_request_api. - */ -export const displayLoginConfirmationDialog = ( paymentRequestType ) => { - if ( ! getPaymentRequestData( 'login_confirmation' ) ) { - return; - } - - let message = getPaymentRequestData( 'login_confirmation' )?.message; - - // Replace dialog text with specific payment request type "Apple Pay" or "Google Pay". - message = message.replace( - /\*\*.*?\*\*/, - paymentRequestType === 'apple_pay' ? 'Apple Pay' : 'Google Pay' - ); - - // Remove asterisks from string. - message = message.replace( /\*\*/g, '' ); - - if ( confirm( message ) ) { - // Redirect to my account page. - window.location.href = getPaymentRequestData( - 'login_confirmation' - )?.redirect_url; - } -}; - -/** - * Parses HTML error notice and returns single error message. - * - * @param {string} notice Error notice DOM HTML. - * @return {string} Error message content - */ -export const getErrorMessageFromNotice = ( notice ) => { - const div = document.createElement( 'div' ); - div.innerHTML = notice.trim(); - return div.firstChild ? div.firstChild.textContent : ''; -}; - -/** - * Searches object for matching key and returns corresponding property value from matched item. - * - * @param {Object} obj Object to search for key. - * @param {string} key Key to match in object. - * @param {string} property Property in object to return correct value. - * @return {int|null} Value to return - */ -const getPropertyByKey = ( obj, key, property ) => { - const foundItem = obj.find( ( item ) => item.key === key ); - return foundItem ? foundItem[ property ] : null; -}; - -/** - * Transforms totals from cartDataContent into format expected by the Store API. - * - * @param {Object} cartDataContent cartData from content component - * @return {Object} Cart totals object for Store API - */ -const constructCartDataContentTotals = ( cartDataContent ) => { - const totals = { - total_items: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_items', - 'value' - )?.toString(), - total_items_tax: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_tax', - 'value' - )?.toString(), - total_fees: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_fees', - 'value' - )?.toString(), - total_fees_tax: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_fees', - 'valueWithTax' - )?.toString(), - total_discount: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_discount', - 'value' - )?.toString(), - total_discount_tax: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_discount', - 'valueWithTax' - )?.toString(), - total_shipping: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_shipping', - 'value' - )?.toString(), - total_shipping_tax: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_shipping', - 'valueWithTax' - )?.toString(), - total_price: cartDataContent.cartTotal.value.toString(), - total_tax: getPropertyByKey( - cartDataContent.cartTotalItems, - 'total_tax', - 'value' - )?.toString(), - currency_code: cartDataContent.currency.code, - currency_symbol: cartDataContent.currency.symbol, - currency_minor_unit: cartDataContent.currency.minorUnit, - currency_decimal_separator: cartDataContent.currency.decimalSeparator, - currency_thousand_separator: cartDataContent.currency.thousandSeparator, - currency_prefix: cartDataContent.currency.prefix, - currency_suffix: cartDataContent.currency.suffix, - }; - - return totals; -}; - -/** - * Transforms the cartData object to the format expected by the Store API. cartData is coming to the blocks Payment Request method - * in two different formats: from the canMakePayment function and from the content component. This function takes in either format - * and transforms it into the format expected by the Store API. - * - * @param {Object|null} cartDataCanMakePayment cartData from canMakePayment function. - * @param {Object|null} cartDataContent cartData from content component. - * @return {Object} Cart totals object. - */ -export const transformCartDataForStoreAPI = ( - cartDataCanMakePayment, - cartDataContent -) => { - let mappedCartData = {}; - - if ( cartDataCanMakePayment ) { - mappedCartData = { - ...cartDataCanMakePayment, - items: cartDataCanMakePayment.cart.cartItems, - totals: cartDataCanMakePayment.cartTotals, - needs_shipping: cartDataCanMakePayment.cartNeedsShipping, - shipping_rates: cartDataCanMakePayment.cart.shippingRates, - }; - } - - if ( cartDataContent ) { - mappedCartData = { - items: cartDataContent.cartItems, - totals: constructCartDataContentTotals( cartDataContent ), - needs_shipping: cartDataContent.needsShipping, - shipping_rates: cartDataContent.shippingRates, - extensions: cartDataContent.extensions, - }; - } - - return mappedCartData; -}; diff --git a/client/tokenized-payment-request/index.js b/client/tokenized-payment-request/index.js deleted file mode 100644 index f1797b725e2..00000000000 --- a/client/tokenized-payment-request/index.js +++ /dev/null @@ -1,90 +0,0 @@ -/* global jQuery */ -/** - * External dependencies - */ -import { applyFilters } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { getUPEConfig } from 'wcpay/utils/checkout'; -import WCPayAPI from '../checkout/api'; -import PaymentRequestCartApi from './cart-api'; -import PaymentRequestOrderApi from './order-api'; -import WooPaymentsPaymentRequest from './payment-request'; -import paymentRequestButtonUi from './button-ui'; -import { getPaymentRequestData } from './frontend-utils'; -import './compatibility/wc-deposits'; -import './compatibility/wc-order-attribution'; -import './compatibility/wc-product-variations'; - -import '../checkout/express-checkout-buttons.scss'; - -jQuery( ( $ ) => { - // Don't load if blocks checkout is being loaded. - if ( - getPaymentRequestData( 'has_block' ) && - getPaymentRequestData( 'button_context' ) !== 'pay_for_order' - ) { - return; - } - - const publishableKey = getPaymentRequestData( 'stripe' ).publishableKey; - - if ( ! publishableKey ) { - // If no configuration is present, we can't do anything. - return; - } - - // initializing the UI's container. - paymentRequestButtonUi.init( { - $container: $( '#wcpay-payment-request-button' ), - } ); - - const api = new WCPayAPI( - { - publishableKey, - accountId: getPaymentRequestData( 'stripe' ).accountId, - locale: getPaymentRequestData( 'stripe' ).locale, - }, - // A promise-based interface to jQuery.post. - ( url, args ) => { - return new Promise( ( resolve, reject ) => { - $.post( url, args ).then( resolve ).fail( reject ); - } ); - } - ); - let paymentRequestCartApi = new PaymentRequestCartApi(); - if ( getPaymentRequestData( 'button_context' ) === 'pay_for_order' ) { - paymentRequestCartApi = new PaymentRequestOrderApi( { - orderId: getUPEConfig( 'order_id' ), - key: getUPEConfig( 'key' ), - billingEmail: getUPEConfig( 'billing_email' ), - } ); - } - - const wooPaymentsPaymentRequest = new WooPaymentsPaymentRequest( { - wcpayApi: api, - paymentRequestCartApi, - productData: getPaymentRequestData( 'product' ) || undefined, - } ); - - wooPaymentsPaymentRequest.init(); - - // When the cart is updated, the PRB is removed from the page and needs to be re-initialized. - $( document.body ).on( 'updated_cart_totals', async () => { - await applyFilters( - 'wcpay.payment-request.update-button-data', - Promise.resolve() - ); - wooPaymentsPaymentRequest.init(); - } ); - - // We need to refresh payment request data when total is updated. - $( document.body ).on( 'updated_checkout', async () => { - await applyFilters( - 'wcpay.payment-request.update-button-data', - Promise.resolve() - ); - } ); -} ); diff --git a/client/tokenized-payment-request/payment-request.js b/client/tokenized-payment-request/payment-request.js deleted file mode 100644 index 046769cfd7d..00000000000 --- a/client/tokenized-payment-request/payment-request.js +++ /dev/null @@ -1,478 +0,0 @@ -/* global jQuery */ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { - doAction, - applyFilters, - removeFilter, - addFilter, -} from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { - setPaymentRequestBranding, - trackPaymentRequestButtonClick, - trackPaymentRequestButtonLoad, -} from './tracking'; -import { - transformStripePaymentMethodForStoreApi, - transformStripeShippingAddressForStoreApi, -} from './transformers/stripe-to-wc'; -import { - transformCartDataForDisplayItems, - transformCartDataForShippingOptions, - transformPrice, -} from './transformers/wc-to-stripe'; -import paymentRequestButtonUi from './button-ui'; -import { - getPaymentRequest, - displayLoginConfirmationDialog, - getPaymentRequestData, -} from './frontend-utils'; -import PaymentRequestCartApi from './cart-api'; -import debounce from './debounce'; - -const noop = () => null; - -/** - * Class to handle Stripe payment forms. - */ -export default class WooPaymentsPaymentRequest { - /** - * Whether the payment was aborted by the customer. - */ - isPaymentAborted = false; - - /** - * Whether global listeners have been added. - */ - areListenersInitialized = false; - - /** - * The cart data represented if the product were to be added to the cart (or, on cart/checkout pages, the cart data itself). - * This is useful on product pages to understand if shipping is needed. - */ - cachedCartData = undefined; - - /** - * API to interface with the cart. - * - * @type {PaymentRequestCartApi} - */ - paymentRequestCartApi = undefined; - - /** - * WCPayAPI instance. - * - * @type {WCPayAPI} - */ - wcpayApi = undefined; - - /** - * On page load for product pages, we might get some data from the backend (which might get overwritten later). - */ - initialProductData = undefined; - - constructor( { wcpayApi, paymentRequestCartApi, productData } ) { - this.wcpayApi = wcpayApi; - this.paymentRequestCartApi = paymentRequestCartApi; - this.initialProductData = productData; - } - - /** - * Starts the payment request - */ - async startPaymentRequest() { - // reference to this class' instance, to be used inside callbacks to avoid `this` misunderstandings. - const _self = this; - const stripe = await this.wcpayApi.getStripe(); - const paymentRequest = getPaymentRequest( { - stripe, - cartData: this.cachedCartData, - productData: this.initialProductData, - } ); - - // Check the availability of the Payment Request API first. - const paymentPermissionResult = await paymentRequest.canMakePayment(); - if ( ! paymentPermissionResult ) { - doAction( 'wcpay.payment-request.availability', { - paymentRequestType: null, - } ); - return; - } - - const buttonBranding = paymentPermissionResult.applePay - ? 'apple_pay' - : 'google_pay'; - - doAction( 'wcpay.payment-request.availability', { - paymentRequestType: buttonBranding, - } ); - - setPaymentRequestBranding( buttonBranding ); - trackPaymentRequestButtonLoad( - getPaymentRequestData( 'button_context' ) - ); - - // on product pages, we need to interact with an anonymous cart to checkout the product, - // so that we don't affect the products in the main cart. - // On cart, checkout, place order pages we instead use the cart itself. - if ( getPaymentRequestData( 'button_context' ) === 'product' ) { - this.paymentRequestCartApi.useSeparateCart(); - } - - const paymentRequestButton = stripe - .elements() - .create( 'paymentRequestButton', { - paymentRequest: paymentRequest, - style: { - paymentRequestButton: { - type: getPaymentRequestData( 'button' ).type, - theme: getPaymentRequestData( 'button' ).theme, - height: getPaymentRequestData( 'button' ).height + 'px', - }, - }, - } ); - paymentRequestButtonUi.showButton( paymentRequestButton ); - - if ( getPaymentRequestData( 'button_context' ) === 'pay_for_order' ) { - paymentRequestButton.on( 'click', () => { - trackPaymentRequestButtonClick( 'pay_for_order' ); - } ); - } - - if ( getPaymentRequestData( 'button_context' ) === 'product' ) { - this.attachPaymentRequestButtonEventListeners(); - } - - removeFilter( - 'wcpay.payment-request.update-button-data', - 'automattic/wcpay/payment-request' - ); - addFilter( - 'wcpay.payment-request.update-button-data', - 'automattic/wcpay/payment-request', - async ( previousPromise ) => { - // Wait for previous filters - await previousPromise; - - const newCartData = await _self.getCartData(); - // checking if items needed shipping, before assigning new cart data. - const didItemsNeedShipping = - _self.initialProductData?.needs_shipping || - _self.cachedCartData?.needs_shipping; - - _self.cachedCartData = newCartData; - - /** - * If the customer aborted the payment request, we need to re init the payment request button to ensure the shipping - * options are re-fetched. If the customer didn't abort the payment request, and the product's shipping status is - * consistent, we can simply update the payment request button with the new total and display items. - */ - if ( - ! _self.isPaymentAborted && - didItemsNeedShipping === newCartData.needs_shipping - ) { - paymentRequest.update( { - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( newCartData.totals.total_price, 10 ) - - parseInt( - newCartData.totals.total_refund || 0, - 10 - ), - newCartData.totals - ), - }, - displayItems: transformCartDataForDisplayItems( - newCartData - ), - } ); - } else { - await _self.init(); - } - } - ); - - if ( getPaymentRequestData( 'button_context' ) === 'product' ) { - const $addToCartButton = jQuery( '.single_add_to_cart_button' ); - - paymentRequestButton.on( 'click', ( event ) => { - trackPaymentRequestButtonClick( 'product' ); - - // If login is required for checkout, display redirect confirmation dialog. - if ( getPaymentRequestData( 'login_confirmation' ) ) { - event.preventDefault(); - displayLoginConfirmationDialog( buttonBranding ); - return; - } - - // First check if product can be added to cart. - if ( $addToCartButton.is( '.disabled' ) ) { - event.preventDefault(); // Prevent showing payment request modal. - if ( - $addToCartButton.is( '.wc-variation-is-unavailable' ) - ) { - window.alert( - window.wc_add_to_cart_variation_params - ?.i18n_unavailable_text || - __( - 'Sorry, this product is unavailable. Please choose a different combination.', - 'woocommerce-payments' - ) - ); - } else { - window.alert( - window?.wc_add_to_cart_variation_params - ?.i18n_make_a_selection_text || - __( - 'Please select some product options before adding this product to your cart.', - 'woocommerce-payments' - ) - ); - } - return; - } - - _self.paymentRequestCartApi.addProductToCart(); - } ); - } - - paymentRequest.on( 'cancel', () => { - _self.isPaymentAborted = true; - - if ( getPaymentRequestData( 'button_context' ) === 'product' ) { - // clearing the cart to avoid issues with products with low or limited availability - // being held hostage by customers cancelling the PRB. - _self.paymentRequestCartApi.emptyCart(); - } - } ); - - paymentRequest.on( 'shippingaddresschange', async ( event ) => { - try { - // Please note that the `event.shippingAddress` might not contain all the fields. - // Some fields might not be present (like `line_1` or `line_2`) due to semi-anonymized data. - const cartData = await _self.paymentRequestCartApi.updateCustomer( - transformStripeShippingAddressForStoreApi( - event.shippingAddress - ) - ); - - const shippingOptions = transformCartDataForShippingOptions( - cartData - ); - - // when no shipping options are returned, the API still returns a 200 status code. - // We need to ensure that shipping options are present - otherwise the PRB dialog won't update correctly. - if ( shippingOptions.length === 0 ) { - event.updateWith( { - // Possible statuses: https://docs.stripe.com/js/appendix/payment_response#payment_response_object-complete - status: 'invalid_shipping_address', - } ); - _self.cachedCartData = cartData; - - return; - } - - event.updateWith( { - // Possible statuses: https://docs.stripe.com/js/appendix/payment_response#payment_response_object-complete - status: 'success', - shippingOptions, - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( - cartData.totals.total_refund || 0, - 10 - ), - cartData.totals - ), - }, - displayItems: transformCartDataForDisplayItems( cartData ), - } ); - - _self.cachedCartData = cartData; - } catch ( error ) { - // Possible statuses: https://docs.stripe.com/js/appendix/payment_response#payment_response_object-complete - event.updateWith( { - status: 'fail', - } ); - } - } ); - - paymentRequest.on( 'shippingoptionchange', async ( event ) => { - try { - const cartData = await _self.paymentRequestCartApi.selectShippingRate( - { package_id: 0, rate_id: event.shippingOption.id } - ); - - event.updateWith( { - status: 'success', - total: { - label: getPaymentRequestData( 'total_label' ), - amount: transformPrice( - parseInt( cartData.totals.total_price, 10 ) - - parseInt( - cartData.totals.total_refund || 0, - 10 - ), - cartData.totals - ), - }, - displayItems: transformCartDataForDisplayItems( cartData ), - } ); - _self.cachedCartData = cartData; - } catch ( error ) { - event.updateWith( { status: 'fail' } ); - } - } ); - - paymentRequest.on( 'paymentmethod', async ( event ) => { - // TODO: this works for PDPs - need to handle checkout scenarios for cart, checkout. - try { - const response = await _self.paymentRequestCartApi.placeOrder( { - // adding extension data as a separate action, - // so that we make it harder for external plugins to modify or intercept checkout data. - ...transformStripePaymentMethodForStoreApi( event ), - extensions: applyFilters( - 'wcpay.payment-request.cart-place-order-extension-data', - {} - ), - } ); - - const confirmationRequest = _self.wcpayApi.confirmIntent( - response.payment_result.redirect_url - ); - // We need to call `complete` before redirecting to close the dialog for 3DS. - event.complete( 'success' ); - - let redirectUrl = ''; - - // `true` means there is no intent to confirm. - if ( confirmationRequest === true ) { - redirectUrl = response.payment_result.redirect_url; - } else { - redirectUrl = await confirmationRequest; - } - - jQuery.blockUI( { - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6, - }, - } ); - - window.location = redirectUrl; - } catch ( error ) { - const response = await error.json(); - event.complete( 'fail' ); - - jQuery( '.woocommerce-error' ).remove(); - - const $container = jQuery( - '.woocommerce-notices-wrapper' - ).first(); - - // the error thrown could have different formats, depending if it was a Store API failure or an ajax failure. - const errorMessage = - response.message || - response.payment_result?.payment_details.find( - ( detail ) => detail.key === 'errorMessage' - )?.value; - if ( $container.length ) { - $container.append( - jQuery( '
' ).text( - errorMessage - ) - ); - - jQuery( 'html, body' ).animate( - { - scrollTop: $container - .find( '.woocommerce-error' ) - .offset().top, - }, - 600 - ); - } - } - } ); - } - - attachPaymentRequestButtonEventListeners() { - if ( this.areListenersInitialized ) { - return; - } - - this.areListenersInitialized = true; - // Block the payment request button as soon as an "input" event is fired, to avoid sync issues - // when the customer clicks on the button before the debounced event is processed. - const $quantityInput = jQuery( '.quantity' ); - const handleQuantityChange = () => { - paymentRequestButtonUi.blockButton(); - }; - $quantityInput.on( 'input', '.qty', handleQuantityChange ); - $quantityInput.on( - 'input', - '.qty', - debounce( 250, async () => { - await applyFilters( - 'wcpay.payment-request.update-button-data', - Promise.resolve() - ); - paymentRequestButtonUi.unblockButton(); - } ) - ); - } - - async getCartData() { - if ( getPaymentRequestData( 'button_context' ) !== 'product' ) { - return await this.paymentRequestCartApi.getCart(); - } - - // creating a new cart and clearing it afterwards, - // to avoid scenarios where the stock for a product with limited (or low) availability is added to the cart, - // preventing other customers from purchasing. - const temporaryCart = new PaymentRequestCartApi(); - temporaryCart.useSeparateCart(); - - const cartData = await temporaryCart.addProductToCart(); - - // no need to wait for the request to end, it can be done asynchronously. - // using `.finally( noop )` to avoid annoying IDE warnings. - temporaryCart.emptyCart().finally( noop ); - - return cartData; - } - - /** - * Initialize event handlers and UI state - */ - async init() { - // on product pages, we should be able to have `initialProductData` from the backend - which saves us some AJAX calls. - if ( ! this.cachedCartData && ! this.initialProductData ) { - try { - this.cachedCartData = await this.getCartData(); - } catch ( e ) { - // if something fails here, we can likely fall back on the `initialProductData`. - } - } - - // once (and if) cart data has been fetched, we can safely clear cached product data. - if ( this.cachedCartData ) { - this.initialProductData = undefined; - } - - await this.startPaymentRequest(); - - // After initializing a new payment request, we need to reset the isPaymentAborted flag. - this.isPaymentAborted = false; - } -} diff --git a/client/tokenized-payment-request/test/payment-request.test.js b/client/tokenized-payment-request/test/payment-request.test.js deleted file mode 100644 index 617384976e5..00000000000 --- a/client/tokenized-payment-request/test/payment-request.test.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addAction, applyFilters, doAction } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import PaymentRequestCartApi from '../cart-api'; -import WooPaymentsPaymentRequest from '../payment-request'; -import { trackPaymentRequestButtonLoad } from '../tracking'; - -jest.mock( '@wordpress/api-fetch', () => jest.fn() ); -jest.mock( '../tracking', () => ( { - setPaymentRequestBranding: () => null, - trackPaymentRequestButtonClick: () => null, - trackPaymentRequestButtonLoad: jest.fn(), -} ) ); - -jest.mock( '../button-ui', () => ( { - showButton: () => null, - blockButton: () => null, - unblockButton: () => null, -} ) ); -jest.mock( '../debounce', () => ( wait, func ) => - function () { - func.apply( this, arguments ); - } -); - -const jQueryMock = ( selector ) => { - if ( typeof selector === 'function' ) { - return selector( jQueryMock ); - } - - return { - on: ( event, callbackOrSelector, callback2 ) => - addAction( - `payment-request-test.jquery-event.${ selector }${ - typeof callbackOrSelector === 'string' - ? `.${ callbackOrSelector }` - : '' - }.${ event }`, - 'tests', - typeof callbackOrSelector === 'string' - ? callback2 - : callbackOrSelector - ), - val: () => null, - is: () => null, - remove: () => null, - }; -}; -jQueryMock.blockUI = () => null; - -describe( 'WooPaymentsPaymentRequest', () => { - let wcpayApi; - - beforeEach( () => { - global.$ = jQueryMock; - global.jQuery = jQueryMock; - global.wcpayPaymentRequestParams = { - nonce: { - store_api_nonce: 'global_store_api_nonce', - }, - button_context: 'cart', - checkout: { - needs_payer_phone: true, - country_code: 'US', - currency_code: 'usd', - }, - total_label: 'wcpay.test (via WooCommerce)', - button: { type: 'default', theme: 'dark', height: '48' }, - }; - wcpayApi = { - getStripe: () => ( { - paymentRequest: () => ( { - update: () => null, - canMakePayment: () => ( { googlePay: true } ), - on: ( event, callback ) => - addAction( - `payment-request-test.registered-action.${ event }`, - 'tests', - callback - ), - } ), - elements: () => ( { - create: () => ( { on: () => null } ), - } ), - } ), - }; - } ); - - afterEach( () => { - jest.resetAllMocks(); - } ); - - it( 'should initialize the Stripe payment request, fire initial tracking, and attach event listeners', async () => { - const headers = new Headers(); - headers.append( 'Nonce', 'nonce-value' ); - - apiFetch.mockResolvedValue( { - headers: headers, - json: () => - Promise.resolve( { - needs_shipping: false, - totals: { - currency_code: 'USD', - total_price: '20', - total_tax: '0', - total_shipping: '5', - }, - items: [ - { name: 'Shirt', quantity: 1, prices: { price: '15' } }, - ], - } ), - } ); - const paymentRequestAvailabilityCallback = jest.fn(); - addAction( - 'wcpay.payment-request.availability', - 'test', - paymentRequestAvailabilityCallback - ); - - const cartApi = new PaymentRequestCartApi(); - const paymentRequest = new WooPaymentsPaymentRequest( { - wcpayApi: wcpayApi, - paymentRequestCartApi: cartApi, - } ); - - expect( paymentRequestAvailabilityCallback ).not.toHaveBeenCalled(); - expect( trackPaymentRequestButtonLoad ).not.toHaveBeenCalled(); - - await paymentRequest.init(); - - expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledTimes( 1 ); - expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledWith( - expect.objectContaining( { paymentRequestType: 'google_pay' } ) - ); - expect( trackPaymentRequestButtonLoad ).toHaveBeenCalledWith( 'cart' ); - - await applyFilters( - 'wcpay.payment-request.update-button-data', - Promise.resolve() - ); - expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledTimes( 1 ); - - // firing this should initialize the button again. - doAction( 'payment-request-test.registered-action.cancel' ); - - await applyFilters( - 'wcpay.payment-request.update-button-data', - Promise.resolve() - ); - expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledTimes( 2 ); - } ); -} ); diff --git a/client/tokenized-payment-request/tracking.js b/client/tokenized-payment-request/tracking.js deleted file mode 100644 index 403160a80fe..00000000000 --- a/client/tokenized-payment-request/tracking.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * External dependencies - */ -import { debounce } from 'lodash'; -import { recordUserEvent } from 'tracks'; - -let paymentRequestBranding; - -// Track the payment request button click event. -export const trackPaymentRequestButtonClick = ( source ) => { - const paymentRequestTypeEvents = { - google_pay: 'gpay_button_click', - apple_pay: 'applepay_button_click', - }; - - const event = paymentRequestTypeEvents[ paymentRequestBranding ]; - if ( ! event ) return; - - recordUserEvent( event, { source } ); -}; - -// Track the payment request button load event. -export const trackPaymentRequestButtonLoad = debounce( ( source ) => { - const paymentRequestTypeEvents = { - google_pay: 'gpay_button_load', - apple_pay: 'applepay_button_load', - }; - - const event = paymentRequestTypeEvents[ paymentRequestBranding ]; - if ( ! event ) return; - - recordUserEvent( event, { source } ); -}, 1000 ); - -export const setPaymentRequestBranding = ( branding ) => - ( paymentRequestBranding = branding ); diff --git a/client/tokenized-payment-request/transformers/stripe-to-wc.js b/client/tokenized-payment-request/transformers/stripe-to-wc.js deleted file mode 100644 index e8902213c33..00000000000 --- a/client/tokenized-payment-request/transformers/stripe-to-wc.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Transform shipping address information from Stripe's address object to - * the cart shipping address object shape. - * - * @param {Object} shippingAddress Stripe's shipping address item - * - * @return {Object} The shipping address in the shape expected by the cart. - */ -export const transformStripeShippingAddressForStoreApi = ( - shippingAddress -) => { - return { - shipping_address: { - first_name: - shippingAddress.recipient - ?.split( ' ' ) - ?.slice( 0, 1 ) - ?.join( ' ' ) ?? '', - last_name: - shippingAddress.recipient - ?.split( ' ' ) - ?.slice( 1 ) - ?.join( ' ' ) ?? '', - company: shippingAddress.organization ?? '', - address_1: shippingAddress.addressLine?.[ 0 ] ?? '', - address_2: shippingAddress.addressLine?.[ 1 ] ?? '', - city: shippingAddress.city ?? '', - state: shippingAddress.region ?? '', - country: shippingAddress.country ?? '', - postcode: shippingAddress.postalCode?.replace( ' ', '' ) ?? '', - }, - }; -}; - -/** - * Transform order data from Stripe's object to the expected format for WC. - * - * @param {Object} paymentData Stripe's order object. - * - * @return {Object} Order object in the format WooCommerce expects. - */ -export const transformStripePaymentMethodForStoreApi = ( paymentData ) => { - const name = - ( paymentData.paymentMethod?.billing_details?.name ?? - paymentData.payerName ) || - ''; - const billing = paymentData.paymentMethod?.billing_details?.address ?? {}; - const shipping = paymentData.shippingAddress ?? {}; - - const paymentRequestType = - paymentData.walletName === 'applePay' ? 'apple_pay' : 'google_pay'; - - const billingPhone = - paymentData.paymentMethod?.billing_details?.phone ?? - paymentData.payerPhone?.replace( '/[() -]/g', '' ) ?? - ''; - return { - customer_note: paymentData.order_comments, - billing_address: { - first_name: name.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '', - last_name: name.split( ' ' )?.slice( 1 )?.join( ' ' ) || '-', - company: billing.organization ?? '', - address_1: billing.line1 ?? '', - address_2: billing.line2 ?? '', - city: billing.city ?? '', - state: billing.state ?? '', - postcode: billing.postal_code ?? '', - country: billing.country ?? '', - email: - paymentData.paymentMethod?.billing_details?.email ?? - paymentData.payerEmail ?? - '', - phone: billingPhone, - }, - // refreshing any shipping address data, now that the customer is placing the order. - shipping_address: { - ...transformStripeShippingAddressForStoreApi( shipping ) - .shipping_address, - // adding the phone number, because it might be needed. - // Stripe doesn't provide us with a different phone number for shipping, so we're going to use the same phone used for billing. - phone: billingPhone, - }, - payment_method: 'woocommerce_payments', - payment_data: [ - { - key: 'payment_method', - value: 'card', - }, - { - key: 'payment_request_type', - value: paymentRequestType, - }, - { - key: 'wcpay-fraud-prevention-token', - value: window.wcpayFraudPreventionToken ?? '', - }, - { - key: 'wcpay-payment-method', - value: paymentData.paymentMethod?.id, - }, - ], - }; -}; diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php deleted file mode 100644 index 82b33593008..00000000000 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ /dev/null @@ -1,873 +0,0 @@ -account = $account; - $this->gateway = $gateway; - $this->express_checkout_helper = $express_checkout_helper; - } - - /** - * Initialize hooks. - * - * @return void - */ - public function init() { - // Checks if WCPay is enabled. - if ( ! $this->gateway->is_enabled() ) { - return; - } - - if ( ! WC_Payments_Features::is_tokenized_cart_ece_enabled() ) { - return; - } - - // Checks if Payment Request is enabled. - if ( 'yes' !== $this->gateway->get_option( 'payment_request' ) ) { - return; - } - - // Don't load for change payment method page. - if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification - return; - } - - add_action( 'template_redirect', [ $this, 'set_session' ] ); - add_action( 'template_redirect', [ $this, 'handle_payment_request_redirect' ] ); - add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); - - add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 ); - add_action( 'woocommerce_checkout_order_processed', [ $this, 'add_order_meta' ], 10, 2 ); - add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); - add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); - add_filter( 'woocommerce_cart_needs_shipping_address', [ $this, 'filter_cart_needs_shipping_address' ], 11, 1 ); - - // Add a filter for the value of `wcpay_is_apple_pay_enabled`. - // This option does not get stored in the database at all, and this function - // will be used to calculate it whenever the option value is retrieved instead. - // It's used for displaying inbox notifications. - add_filter( 'pre_option_wcpay_is_apple_pay_enabled', [ $this, 'get_option_is_apple_pay_enabled' ], 10, 1 ); - } - - /** - * Checks whether authentication is required for checkout. - * - * @return bool - */ - public function is_authentication_required() { - // If guest checkout is disabled and account creation is not possible, authentication is required. - if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) { - return true; - } - // If cart contains subscription and account creation is not posible, authentication is required. - if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) { - return true; - } - - return false; - } - - /** - * Checks whether account creation is possible during checkout. - * - * @return bool - */ - public function is_account_creation_possible() { - $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' ); - - // If a subscription is being purchased, check if account creation is allowed for subscriptions. - if ( ! $is_signup_from_checkout_allowed && $this->has_subscription_product() ) { - $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_from_checkout_for_subscriptions', 'no' ); - } - - // If automatically generate username/password are disabled, the Payment Request API - // can't include any of those fields, so account creation is not possible. - return ( - $is_signup_from_checkout_allowed && - 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && - 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' ) - ); - } - - /** - * Sets the WC customer session if one is not set. - * This is needed so nonces can be verified by AJAX Request. - * - * @return void - */ - public function set_session() { - // Don't set session cookies on product pages to allow for caching when payment request - // buttons are disabled. But keep cookies if there is already an active WC session in place. - if ( - ! ( $this->express_checkout_helper->is_product() && $this->should_show_payment_request_button() ) - || ( isset( WC()->session ) && WC()->session->has_session() ) - ) { - return; - } - - WC()->session->set_customer_session_cookie( true ); - } - - /** - * Handles payment request redirect when the redirect dialog "Continue" button is clicked. - */ - public function handle_payment_request_redirect() { - if ( - ! empty( $_GET['wcpay_payment_request_redirect_url'] ) - && ! empty( $_GET['_wpnonce'] ) - && wp_verify_nonce( $_GET['_wpnonce'], 'wcpay-set-redirect-url' ) // @codingStandardsIgnoreLine - ) { - $url = rawurldecode( esc_url_raw( wp_unslash( $_GET['wcpay_payment_request_redirect_url'] ) ) ); - // Sets a redirect URL cookie for 10 minutes, which we will redirect to after authentication. - // Users will have a 10 minute timeout to login/create account, otherwise redirect URL expires. - wc_setcookie( 'wcpay_payment_request_redirect_url', $url, time() + MINUTE_IN_SECONDS * 10 ); - // Redirects to "my-account" page. - wp_safe_redirect( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ) ); - } - } - - /** - * The settings for the `button` attribute - they depend on the "grouped settings" flag value. - * - * @return array - */ - public function get_button_settings() { - $button_type = $this->gateway->get_option( 'payment_request_button_type' ); - $common_settings = $this->express_checkout_helper->get_common_button_settings(); - $payment_request_button_settings = [ - // Default format is en_US. - 'locale' => apply_filters( 'wcpay_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), - 'branded_type' => 'default' === $button_type ? 'short' : 'long', - ]; - - return array_merge( $common_settings, $payment_request_button_settings ); - } - - /** - * Gets the product total price. - * - * @param object $product WC_Product_* object. - * @param bool $is_deposit Whether customer is paying a deposit. - * @param int $deposit_plan_id The ID of the deposit plan. - * - * @return mixed Total price. - * - * @throws Invalid_Price_Exception Whenever a product has no price. - */ - public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) { - // If prices should include tax, using tax inclusive price. - if ( $this->express_checkout_helper->cart_prices_include_tax() ) { - $base_price = wc_get_price_including_tax( $product ); - } else { - $base_price = wc_get_price_excluding_tax( $product ); - } - - // If WooCommerce Deposits is active, we need to get the correct price for the product. - if ( class_exists( 'WC_Deposits_Product_Manager' ) && class_exists( 'WC_Deposits_Plans_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) { - // If is_deposit is null, we use the default deposit type for the product. - if ( is_null( $is_deposit ) ) { - $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() ); - } - if ( $is_deposit ) { - $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() ); - $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() ); - // Default to first (default) plan if no plan is specified. - if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) { - $deposit_plan_id = $available_plan_ids[0]; - } - - // Ensure the selected plan is available for the product. - if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) { - $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price ); - } - } - } - - // Add subscription sign-up fees to product price. - $sign_up_fee = 0; - $subscription_types = [ - 'subscription', - 'subscription_variation', - ]; - if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) { - // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0. - $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product ); - } - - if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) { - $error_message = sprintf( - // Translators: %d is the numeric ID of the product without a price. - __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ), - (int) $product->get_id() - ); - throw new Invalid_Price_Exception( - esc_html( $error_message ) - ); - } - - return $base_price + $sign_up_fee; - } - - /** - * Gets the product data for the currently viewed page. - * - * @return mixed Returns false if not on a product page, the product information otherwise. - */ - public function get_product_data() { - if ( ! $this->express_checkout_helper->is_product() ) { - return false; - } - - /** @var WC_Product_Variable $product */ // phpcs:ignore - $product = $this->express_checkout_helper->get_product(); - $currency = get_woocommerce_currency(); - - if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) { - $variation_attributes = $product->get_variation_attributes(); - $attributes = []; - - foreach ( $variation_attributes as $attribute_name => $attribute_values ) { - $attribute_key = 'attribute_' . sanitize_title( $attribute_name ); - - // Passed value via GET takes precedence. Otherwise get the default value for given attribute. - $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification - ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification - : $product->get_variation_default_attribute( $attribute_name ); - } - - $data_store = WC_Data_Store::load( 'product' ); - $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); - - if ( ! empty( $variation_id ) ) { - $product = wc_get_product( $variation_id ); - } - } - - try { - $price = $this->get_product_price( $product ); - } catch ( Invalid_Price_Exception $e ) { - Logger::log( $e->getMessage() ); - - return false; - } - - $data = []; - $items = []; - - $items[] = [ - 'label' => $product->get_name(), - 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ), - ]; - - $total_tax = 0; - foreach ( $this->get_taxes_like_cart( $product, $price ) as $tax ) { - $total_tax += $tax; - - $items[] = [ - 'label' => __( 'Tax', 'woocommerce-payments' ), - 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ), - 'pending' => 0 === $tax, - ]; - } - - if ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ) { - $items[] = [ - 'label' => __( 'Shipping', 'woocommerce-payments' ), - 'amount' => 0, - 'pending' => true, - ]; - - $data['shippingOptions'] = [ - 'id' => 'pending', - 'label' => __( 'Pending', 'woocommerce-payments' ), - 'detail' => '', - 'amount' => 0, - ]; - } - - $data['displayItems'] = $items; - $data['total'] = [ - 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->express_checkout_helper->get_total_label() ), - 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ), - 'pending' => true, - ]; - - $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ); - $data['currency'] = strtolower( $currency ); - $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); - - return apply_filters( 'wcpay_payment_request_product_data', $data, $product ); - } - - /** - * Filters the gateway title to reflect Payment Request type - * - * @param string $title Gateway title. - * @param string $id Gateway ID. - */ - public function filter_gateway_title( $title, $id ) { - if ( 'woocommerce_payments' !== $id || ! is_admin() ) { - return $title; - } - - $order = $this->get_current_order(); - $method_title = is_object( $order ) ? $order->get_payment_method_title() : ''; - - if ( ! empty( $method_title ) ) { - if ( - strpos( $method_title, 'Apple Pay' ) === 0 - || strpos( $method_title, 'Google Pay' ) === 0 - || strpos( $method_title, 'Payment Request' ) === 0 - ) { - return $method_title; - } - } - - return $title; - } - - /** - * Used to get the order in admin edit page. - * - * @return WC_Order|WC_Order_Refund|bool - */ - private function get_current_order() { - global $theorder; - global $post; - - if ( is_object( $theorder ) ) { - return $theorder; - } - - if ( is_object( $post ) ) { - return wc_get_order( $post->ID ); - } - - return false; - } - - /** - * Normalizes postal code in case of redacted data from Apple Pay. - * - * @param string $postcode Postal code. - * @param string $country Country. - */ - public function get_normalized_postal_code( $postcode, $country ) { - /** - * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively - * when passing it back from the shippingcontactselected object. This causes WC to invalidate - * the postal code and not calculate shipping zones correctly. - */ - if ( Country_Code::UNITED_KINGDOM === $country ) { - // Replaces a redacted string with something like N1C0000. - return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '0' ); - } - if ( Country_Code::CANADA === $country ) { - // Replaces a redacted string with something like H3B000. - return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' ); - } - - return $postcode; - } - - /** - * Add needed order meta - * - * @param integer $order_id The order ID. - * - * @return void - */ - public function add_order_meta( $order_id ) { - if ( empty( $_POST['payment_request_type'] ) || ! isset( $_POST['payment_method'] ) || 'woocommerce_payments' !== $_POST['payment_method'] ) { // phpcs:ignore WordPress.Security.NonceVerification - return; - } - - $order = wc_get_order( $order_id ); - - $payment_request_type = wc_clean( wp_unslash( $_POST['payment_request_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification - - $payment_method_titles = [ - 'apple_pay' => 'Apple Pay', - 'google_pay' => 'Google Pay', - ]; - - $suffix = apply_filters( 'wcpay_payment_request_payment_method_title_suffix', 'WooPayments' ); - if ( ! empty( $suffix ) ) { - $suffix = " ($suffix)"; - } - - $payment_method_title = isset( $payment_method_titles[ $payment_request_type ] ) ? $payment_method_titles[ $payment_request_type ] : 'Payment Request'; - $order->set_payment_method_title( $payment_method_title . $suffix ); - $order->save(); - } - - /** - * Checks whether Payment Request Button should be available on this page. - * - * @return bool - */ - public function should_show_payment_request_button() { - // If account is not connected, then bail. - if ( ! $this->account->is_stripe_connected() ) { - return false; - } - - // If no SSL, bail. - if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) { - Logger::log( 'Stripe Payment Request live mode requires SSL.' ); - - return false; - } - - // Page not supported. - if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) { - return false; - } - - // Product page, but not available in settings. - if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) { - return false; - } - - // Checkout page, but not available in settings. - if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) { - return false; - } - - // Cart page, but not available in settings. - if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) { - return false; - } - - // Product page, but has unsupported product type. - if ( $this->express_checkout_helper->is_product() && ! apply_filters( 'wcpay_payment_request_is_product_supported', $this->is_product_supported(), $this->express_checkout_helper->get_product() ) ) { - Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' ); - - return false; - } - - // Cart has unsupported product type. - if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { - Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' ); - - return false; - } - - // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons. - if ( $this->express_checkout_helper->is_pay_for_order_page() ) { - return true; - } - - // Cart total is 0 or is on product page and product price is 0. - // Exclude pay-for-order pages from this check. - if ( - ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) || - ( $this->express_checkout_helper->is_product() && 0.0 === (float) $this->express_checkout_helper->get_product()->get_price() ) - - ) { - Logger::log( 'Order price is 0 ( Payment Request button disabled )' ); - - return false; - } - - return true; - } - - /** - * Checks to make sure product type is supported. - * - * @return array - */ - public function supported_product_types() { - return apply_filters( - 'wcpay_payment_request_supported_types', - [ - 'simple', - 'variable', - 'variation', - 'subscription', - 'variable-subscription', - 'subscription_variation', - 'booking', - 'bundle', - 'composite', - 'mix-and-match', - ] - ); - } - - /** - * Checks the cart to see if all items are allowed to be used. - * - * @return boolean - */ - public function has_allowed_items_in_cart() { - // Pre Orders compatbility where we don't support charge upon release. - if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) { - return false; - } - - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); - - if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) { - return false; - } - - /** - * Filter whether product supports Payment Request Button on cart page. - * - * @param boolean $is_supported Whether product supports Payment Request Button on cart page. - * @param object $_product Product object. - * - * @since 6.9.0 - */ - if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) { - return false; - } - - // Trial subscriptions with shipping are not supported. - if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) { - return false; - } - } - - // We don't support multiple packages with Payment Request Buttons because we can't offer a good UX. - $packages = WC()->cart->get_shipping_packages(); - if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) { - return false; - } - - return true; - } - - /** - * Checks whether cart contains a subscription product or this is a subscription product page. - * - * @return boolean - */ - public function has_subscription_product() { - if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { - return false; - } - - if ( $this->express_checkout_helper->is_product() ) { - $product = $this->express_checkout_helper->get_product(); - if ( WC_Subscriptions_Product::is_subscription( $product ) ) { - return true; - } - } - - if ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) { - if ( WC_Subscriptions_Cart::cart_contains_subscription() ) { - return true; - } - } - - return false; - } - - /** - * Returns the login redirect URL. - * - * @param string $redirect Default redirect URL. - * - * @return string Redirect URL. - */ - public function get_login_redirect_url( $redirect ) { - $url = esc_url_raw( wp_unslash( $_COOKIE['wcpay_payment_request_redirect_url'] ?? '' ) ); - - if ( empty( $url ) ) { - return $redirect; - } - wc_setcookie( 'wcpay_payment_request_redirect_url', '' ); - - return $url; - } - - /** - * Load public scripts and styles. - */ - public function scripts() { - // Don't load scripts if page is not supported. - if ( ! $this->should_show_payment_request_button() ) { - return; - } - - $payment_request_params = [ - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'stripe' => [ - 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), - 'accountId' => $this->account->get_stripe_account_id(), - 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), - ], - 'nonce' => [ - 'get_cart_details' => wp_create_nonce( 'wcpay-get-cart-details' ), - 'shipping' => wp_create_nonce( 'wcpay-payment-request-shipping' ), - 'update_shipping' => wp_create_nonce( 'wcpay-update-shipping-method' ), - 'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ), - 'add_to_cart' => wp_create_nonce( 'wcpay-add-to-cart' ), - 'empty_cart' => wp_create_nonce( 'wcpay-empty-cart' ), - 'get_selected_product_data' => wp_create_nonce( 'wcpay-get-selected-product-data' ), - 'platform_tracker' => wp_create_nonce( 'platform_tracks_nonce' ), - 'pay_for_order' => wp_create_nonce( 'pay_for_order' ), - 'tokenized_cart_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_nonce' ), - 'tokenized_cart_session_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ), - 'store_api_nonce' => wp_create_nonce( 'wc_store_api' ), - ], - 'checkout' => [ - 'currency_code' => strtolower( get_woocommerce_currency() ), - 'currency_decimals' => WC_Payments::get_localization_service()->get_currency_format( get_woocommerce_currency() )['num_decimals'], - 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), - 'needs_shipping' => WC()->cart->needs_shipping(), - // Defaults to 'required' to match how core initializes this option. - 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), - ], - 'button' => $this->get_button_settings(), - 'login_confirmation' => $this->get_login_confirmation_settings(), - 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ), - 'product' => $this->get_product_data(), - 'total_label' => $this->express_checkout_helper->get_total_label(), - 'button_context' => $this->express_checkout_helper->get_button_context(), - 'is_product_page' => $this->express_checkout_helper->is_product(), - 'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(), - 'is_checkout_page' => $this->express_checkout_helper->is_checkout(), - ]; - - if ( WC_Payments_Features::is_tokenized_cart_ece_enabled() ) { - WC_Payments::register_script_with_dependencies( - 'WCPAY_PAYMENT_REQUEST', - 'dist/tokenized-payment-request', - [ - 'jquery', - 'stripe', - ] - ); - WC_Payments_Utils::enqueue_style( - 'WCPAY_PAYMENT_REQUEST', - plugins_url( 'dist/tokenized-payment-request.css', WCPAY_PLUGIN_FILE ), - [], - WC_Payments::get_file_version( 'dist/tokenized-payment-request.css' ) - ); - } - - wp_localize_script( 'WCPAY_PAYMENT_REQUEST', 'wcpayPaymentRequestParams', $payment_request_params ); - - wp_set_script_translations( 'WCPAY_PAYMENT_REQUEST', 'woocommerce-payments' ); - - wp_enqueue_script( 'WCPAY_PAYMENT_REQUEST' ); - - Fraud_Prevention_Service::maybe_append_fraud_prevention_token(); - - $gateways = WC()->payment_gateways->get_available_payment_gateways(); - if ( isset( $gateways['woocommerce_payments'] ) ) { - WC_Payments::get_wc_payments_checkout()->register_scripts(); - } - } - - /** - * Display the payment request button. - */ - public function display_payment_request_button_html() { - if ( ! $this->should_show_payment_request_button() ) { - return; - } - ?> -
- -
- express_checkout_helper->get_product(); - if ( is_null( $product ) ) { - return false; - } - - if ( ! is_object( $product ) ) { - return false; - } - - if ( ! in_array( $product->get_type(), $this->supported_product_types(), true ) ) { - return false; - } - - // Trial subscriptions with shipping are not supported. - if ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) { - return false; - } - - // Pre Orders charge upon release not supported. - if ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) { - return false; - } - - // Composite products are not supported on the product page. - if ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) { - return false; - } - - // Mix and match products are not supported on the product page. - if ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) { - return false; - } - - if ( class_exists( 'WC_Product_Addons_Helper' ) ) { - // File upload addon not supported. - $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); - foreach ( $product_addons as $addon ) { - if ( 'file_upload' === $addon['type'] ) { - return false; - } - } - } - - return true; - } - - /** - * Determine wether to filter the cart needs shipping address. - * - * @param boolean $needs_shipping_address Whether the cart needs a shipping address. - */ - public function filter_cart_needs_shipping_address( $needs_shipping_address ) { - if ( $this->has_subscription_product() && wc_get_shipping_method_count( true, true ) === 0 ) { - return false; - } - - return $needs_shipping_address; - } - - /** - * Calculates whether Apple Pay is enabled for this store. - * The option value is not stored in the database, and is calculated - * using this function instead, and the values is returned by using the pre_option filter. - * - * The option value is retrieved for inbox notifications. - * - * @param mixed $value The value of the option. - */ - public function get_option_is_apple_pay_enabled( $value ) { - // Return a random value (1 or 2) if the account is live and payment request buttons are enabled. - if ( - $this->gateway->is_enabled() - && 'yes' === $this->gateway->get_option( 'payment_request' ) - && ! WC_Payments::mode()->is_dev() - && $this->account->get_is_live() - ) { - $value = wp_rand( 1, 2 ); - } - - return $value; - } - - /** - * Settings array for the user authentication dialog and redirection. - * - * @return array|false - */ - public function get_login_confirmation_settings() { - if ( is_user_logged_in() || ! $this->is_authentication_required() ) { - return false; - } - - /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */ - $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-payments' ); - $redirect_url = add_query_arg( - [ - '_wpnonce' => wp_create_nonce( 'wcpay-set-redirect-url' ), - 'wcpay_payment_request_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ), - // Current URL to redirect to after login. - ], - home_url() - ); - - return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- home_url passed in to add_query_arg. - 'message' => $message, - 'redirect_url' => $redirect_url, - ]; - } - - /** - * Calculates taxes as displayed on cart, based on a product and a particular price. - * - * @param WC_Product $product The product, for retrieval of tax classes. - * @param float $price The price, which to calculate taxes for. - * - * @return array An array of final taxes. - */ - private function get_taxes_like_cart( $product, $price ) { - if ( ! wc_tax_enabled() || $this->express_checkout_helper->cart_prices_include_tax() ) { - // Only proceed when taxes are enabled, but not included. - return []; - } - - // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works. - $tax_class = $product->get_tax_class(); - $rates = WC_Tax::get_rates( $tax_class ); - // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here. - - // Normally there should be a single tax, but `calc_tax` returns an array, let's use it. - return WC_Tax::calc_tax( $price, $rates, false ); - } -} diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 2a5164e8af3..d0692a8a2b8 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -24,13 +24,6 @@ WC_Pre_Orders_Product - - - WC_Pre_Orders_Product - WC_Subscriptions_Product - WC_Subscriptions_Cart - - WC_Subscriptions_Cart diff --git a/tests/js/jest.config.js b/tests/js/jest.config.js index b81f434b8c5..7a918a8d63c 100644 --- a/tests/js/jest.config.js +++ b/tests/js/jest.config.js @@ -45,8 +45,6 @@ module.exports = { '/.*/build-module/', '/docker/', '/tests/e2e', - // We'll delete the directory and its contents as part of https://github.com/Automattic/woocommerce-payments/issues/9722 . - '/client/tokenized-payment-request', ], transform: { ...tsjPreset.transform, diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 99f99b071c2..89ef79bfb11 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -96,8 +96,6 @@ function () { require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-refunds-controller.php'; - require_once $_plugin_dir . 'includes/class-wc-payments-payment-request-button-handler.php'; - // Load currency helper class early to ensure its implementation is used over the one resolved during further test initialization. require_once __DIR__ . '/helpers/class-wc-helper-site-currency.php'; diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php deleted file mode 100644 index b34299b76f6..00000000000 --- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php +++ /dev/null @@ -1,650 +0,0 @@ - Country_Code::UNITED_STATES, - 'state' => 'CA', - 'postcode' => '94110', - 'city' => 'San Francisco', - 'address_1' => '60 29th Street', - 'address_2' => '#343', - ]; - - /** - * Mock WC_Payments_API_Client. - * - * @var WC_Payments_API_Client - */ - private $mock_api_client; - - /** - * Payment request instance. - * - * @var WC_Payments_Payment_Request_Button_Handler - */ - private $pr; - - /** - * WC_Payments_Account instance. - * - * @var WC_Payments_Account - */ - private $mock_wcpay_account; - - /** - * Test product to add to the cart - * @var WC_Product_Simple - */ - private $simple_product; - - /** - * Test shipping zone. - * - * @var WC_Shipping_Zone - */ - private $zone; - - /** - * Flat rate shipping method instance id - * - * @var int - */ - private $flat_rate_id; - - /** - * Flat rate shipping method instance id - * - * @var int - */ - private $local_pickup_id; - - /** - * Used to get the settings. - * - * @var WC_Payment_Gateway_WCPay - */ - private $mock_wcpay_gateway; - - /** - * Express Checkout Helper instance. - * - * @var WC_Payments_Express_Checkout_Button_Helper - */ - private $express_checkout_helper; - - /** - * Sets up things all tests need. - */ - public function set_up() { - parent::set_up(); - add_filter( 'pre_option_woocommerce_tax_based_on', [ $this, '__return_base' ] ); - - $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) - ->disableOriginalConstructor() - ->setMethods( - [ - 'get_account_data', - 'is_server_connected', - 'capture_intention', - 'cancel_intention', - 'get_payment_method', - ] - ) - ->getMock(); - $this->mock_api_client->expects( $this->any() )->method( 'is_server_connected' )->willReturn( true ); - $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); - - $this->mock_wcpay_gateway = $this->make_wcpay_gateway(); - - $this->express_checkout_helper = $this->getMockBuilder( WC_Payments_Express_Checkout_Button_Helper::class ) - ->setMethods( - [ - 'is_product', - 'get_product', - ] - ) - ->setConstructorArgs( [ $this->mock_wcpay_gateway, $this->mock_wcpay_account ] ) - ->getMock(); - - $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper ); - - $this->simple_product = WC_Helper_Product::create_simple_product(); - - WC_Helper_Shipping::delete_simple_flat_rate(); - $zone = new WC_Shipping_Zone(); - $zone->set_zone_name( 'Worldwide' ); - $zone->set_zone_order( 1 ); - $zone->save(); - - add_filter( - 'woocommerce_find_rates', - function () { - return [ - 1 => - [ - 'rate' => 10.0, - 'label' => 'Tax', - 'shipping' => 'yes', - 'compound' => 'no', - ], - ]; - }, - 50, - 2 - ); - - $this->flat_rate_id = $zone->add_shipping_method( 'flat_rate' ); - self::set_shipping_method_cost( $this->flat_rate_id, '5' ); - - $this->local_pickup_id = $zone->add_shipping_method( 'local_pickup' ); - self::set_shipping_method_cost( $this->local_pickup_id, '1' ); - - $this->zone = $zone; - - WC()->session->init(); - WC()->cart->add_to_cart( $this->simple_product->get_id(), 1 ); - WC()->cart->calculate_totals(); - } - - public function tear_down() { - WC_Subscriptions_Cart::set_cart_contains_subscription( false ); - WC()->cart->empty_cart(); - WC()->session->cleanup_sessions(); - $this->zone->delete(); - delete_option( 'woocommerce_woocommerce_payments_settings' ); - remove_filter( 'pre_option_woocommerce_tax_based_on', [ $this, '__return_base' ] ); - remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_excl' ] ); - remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] ); - remove_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, '__return_excl' ] ); - remove_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, '__return_incl' ] ); - remove_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, '__return_yes' ] ); - remove_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, '__return_no' ] ); - remove_filter( 'wc_tax_enabled', '__return_true' ); - remove_filter( 'wc_tax_enabled', '__return_false' ); - remove_filter( 'wc_shipping_enabled', '__return_false' ); - remove_all_filters( 'woocommerce_find_rates' ); - - parent::tear_down(); - } - - public function __return_yes() { - return 'yes'; - } - - public function __return_no() { - return 'no'; - } - - public function __return_excl() { - return 'excl'; - } - - public function __return_incl() { - return 'incl'; - } - - public function __return_base() { - return 'base'; - } - - /** - * @return WC_Payment_Gateway_WCPay - */ - private function make_wcpay_gateway() { - $mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class ); - $mock_token_service = $this->createMock( WC_Payments_Token_Service::class ); - $mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); - $mock_rate_limiter = $this->createMock( Session_Rate_Limiter::class ); - $mock_order_service = $this->createMock( WC_Payments_Order_Service::class ); - $mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class ); - $mock_payment_method = $this->createMock( CC_Payment_Method::class ); - - return new WC_Payment_Gateway_WCPay( - $this->mock_api_client, - $this->mock_wcpay_account, - $mock_customer_service, - $mock_token_service, - $mock_action_scheduler_service, - $mock_payment_method, - [ 'card' => $mock_payment_method ], - $mock_order_service, - $mock_dpps, - $this->createMock( WC_Payments_Localization_Service::class ), - $this->createMock( WC_Payments_Fraud_Service::class ), - $this->createMock( Duplicates_Detection_Service::class ), - $mock_rate_limiter - ); - } - - /** - * Sets shipping method cost - * - * @param string $instance_id Shipping method instance id - * @param string $cost Shipping method cost in USD - */ - private static function set_shipping_method_cost( $instance_id, $cost ) { - $method = WC_Shipping_Zones::get_shipping_method( $instance_id ); - $option_key = $method->get_instance_option_key(); - $options = get_option( $option_key ); - $options['cost'] = $cost; - update_option( $option_key, $options ); - } - - /** - * Retrieves rate id by shipping method instance id. - * - * @param string $instance_id Shipping method instance id. - * - * @return string Shipping option instance rate id. - */ - private static function get_shipping_option_rate_id( $instance_id ) { - $method = WC_Shipping_Zones::get_shipping_method( $instance_id ); - - return $method->get_rate_id(); - } - - public function test_multiple_packages_in_cart_not_allowed() { - // Add fake packages to the cart. - add_filter( - 'woocommerce_cart_shipping_packages', - function () { - return [ - 'fake_package_1', - 'fake_package_2', - ]; - } - ); - $this->mock_wcpay_gateway = $this->make_wcpay_gateway(); - $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper ); - - $this->assertFalse( $this->pr->has_allowed_items_in_cart() ); - } - - public function test_get_product_price_returns_simple_price() { - $this->assertEquals( - $this->simple_product->get_price(), - $this->pr->get_product_price( $this->simple_product ) - ); - } - - public function test_get_product_price_returns_deposit_amount() { - $product_price = 10; - $this->simple_product->set_price( $product_price ); - - $this->assertEquals( - $product_price, - $this->pr->get_product_price( $this->simple_product, false ), - 'When deposit is disabled, the regular price should be returned.' - ); - $this->assertEquals( - $product_price, - $this->pr->get_product_price( $this->simple_product, true ), - 'When deposit is enabled, but the product has no setting for deposit, the regular price should be returned.' - ); - - $this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' ); - $this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' ); - $this->simple_product->update_meta_data( '_wc_deposit_amount', 50 ); - $this->simple_product->save_meta_data(); - - $this->assertEquals( - $product_price, - $this->pr->get_product_price( $this->simple_product, false ), - 'When deposit is disabled, the regular price should be returned.' - ); - $this->assertEquals( - $product_price * 0.5, - $this->pr->get_product_price( $this->simple_product, true ), - 'When deposit is enabled, the deposit price should be returned.' - ); - - $this->simple_product->delete_meta_data( '_wc_deposit_amount' ); - $this->simple_product->delete_meta_data( '_wc_deposit_type' ); - $this->simple_product->delete_meta_data( '_wc_deposit_enabled' ); - $this->simple_product->save_meta_data(); - } - - public function test_get_product_price_returns_deposit_amount_default_values() { - $product_price = 10; - $this->simple_product->set_price( $product_price ); - - $this->assertEquals( - $product_price, - $this->pr->get_product_price( $this->simple_product ), - 'When deposit is disabled by default, the regular price should be returned.' - ); - - $this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' ); - $this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' ); - $this->simple_product->update_meta_data( '_wc_deposit_amount', 50 ); - $this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'full' ); - $this->simple_product->save_meta_data(); - - $this->assertEquals( - $product_price, - $this->pr->get_product_price( $this->simple_product ), - 'When deposit is optional and disabled by default, the regular price should be returned.' - ); - - $this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'deposit' ); - $this->simple_product->save_meta_data(); - - $this->assertEquals( - $product_price * 0.5, - $this->pr->get_product_price( $this->simple_product ), - 'When deposit is optional and selected by default, the deposit price should be returned.' - ); - } - - /** - * @dataProvider provide_get_product_tax_tests - */ - public function test_get_product_price_returns_price_with_tax( $tax_enabled, $prices_include_tax, $tax_display_shop, $tax_display_cart, $product_price, $expected_price ) { - $this->simple_product->set_price( $product_price ); - add_filter( 'wc_tax_enabled', $tax_enabled ? '__return_true' : '__return_false' ); // reset in tear_down. - add_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, "__return_$prices_include_tax" ] ); // reset in tear_down. - add_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, "__return_$tax_display_shop" ] ); // reset in tear_down. - add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, "__return_$tax_display_cart" ] ); // reset in tear_down. - WC()->cart->calculate_totals(); - $this->assertEquals( - $expected_price, - $this->pr->get_product_price( $this->simple_product ) - ); - } - - public function provide_get_product_tax_tests() { - yield 'Tax Disabled, Price Display Unaffected' => [ - 'tax_enabled' => false, - 'prices_include_tax' => 'no', - 'tax_display_shop' => 'excl', - 'tax_display_cart' => 'incl', - 'product_price' => 10, - 'expected_price' => 10, - ]; - - // base prices DO NOT include tax. - - yield 'Shop: Excl / Cart: Incl, Base Prices Don\'t Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'no', - 'tax_display_shop' => 'excl', - 'tax_display_cart' => 'incl', - 'product_price' => 10, - 'expected_price' => 11, - ]; - yield 'Shop: Excl / Cart: Excl, Base Prices Don\'t Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'no', - 'tax_display_shop' => 'excl', - 'tax_display_cart' => 'excl', - 'product_price' => 10, - 'expected_price' => 10, - ]; - - yield 'Shop: Incl / Cart: Incl, Base Prices Don\'t Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'no', - 'tax_display_shop' => 'incl', - 'tax_display_cart' => 'incl', - 'product_price' => 10, - 'expected_price' => 11, - ]; - yield 'Shop: Incl / Cart: Excl, Base Prices Don\'t Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'no', - 'tax_display_shop' => 'incl', - 'tax_display_cart' => 'excl', - 'product_price' => 10, - 'expected_price' => 10, - ]; - - // base prices include tax. - - yield 'Shop: Excl / Cart: Incl, Base Prices Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'yes', - 'tax_display_shop' => 'excl', - 'tax_display_cart' => 'incl', - 'product_price' => 10, - 'expected_price' => 10, - ]; - yield 'Shop: Excl / Cart: Excl, Base Prices Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'yes', - 'tax_display_shop' => 'excl', - 'tax_display_cart' => 'excl', - 'product_price' => 10, - 'expected_price' => 9.090909, - ]; - - yield 'Shop: Incl / Cart: Incl, Base Prices Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'yes', - 'tax_display_shop' => 'incl', - 'tax_display_cart' => 'incl', - 'product_price' => 10, - 'expected_price' => 10, - ]; - yield 'Shop: Incl / Cart: Excl, Base Prices Include Tax' => [ - 'tax_enabled' => true, - 'prices_include_tax' => 'yes', - 'tax_display_shop' => 'incl', - 'tax_display_cart' => 'excl', - 'product_price' => 10, - 'expected_price' => 9.090909, - ]; - } - - public function test_get_product_price_includes_subscription_sign_up_fee() { - $mock_product = $this->create_mock_subscription( 'subscription' ); - add_filter( - 'test_deposit_get_product', - function () use ( $mock_product ) { - return $mock_product; - } - ); - - // We have a helper because we are not loading subscriptions. - WC_Subscriptions_Product::set_sign_up_fee( 10 ); - - $this->assertEquals( 20, $this->pr->get_product_price( $mock_product ) ); - - // Restore the sign-up fee after the test. - WC_Subscriptions_Product::set_sign_up_fee( 0 ); - - remove_all_filters( 'test_deposit_get_product' ); - } - - public function test_get_product_price_includes_variable_subscription_sign_up_fee() { - $mock_product = $this->create_mock_subscription( 'subscription_variation' ); - add_filter( - 'test_deposit_get_product', - function () use ( $mock_product ) { - return $mock_product; - } - ); - - // We have a helper because we are not loading subscriptions. - WC_Subscriptions_Product::set_sign_up_fee( 10 ); - - $this->assertEquals( 20, $this->pr->get_product_price( $mock_product ) ); - - // Restore the sign-up fee after the test. - WC_Subscriptions_Product::set_sign_up_fee( 0 ); - - remove_all_filters( 'test_deposit_get_product' ); - } - - public function test_get_product_price_throws_exception_for_products_without_prices() { - if ( version_compare( WC_VERSION, '6.9.0', '>=' ) ) { - $this->markTestSkipped( 'This test is useless starting with WooCommerce 6.9.0' ); - return; - } - - $this->simple_product->set_price( 'a' ); - - $this->expectException( WCPay\Exceptions\Invalid_Price_Exception::class ); - - $this->pr->get_product_price( $this->simple_product ); - } - - public function test_get_product_price_throws_exception_for_a_non_numeric_signup_fee() { - $mock_product = $this->create_mock_subscription( 'subscription' ); - add_filter( - 'test_deposit_get_product', - function () use ( $mock_product ) { - return $mock_product; - } - ); - WC_Subscriptions_Product::set_sign_up_fee( 'a' ); - - $this->expectException( WCPay\Exceptions\Invalid_Price_Exception::class ); - $this->pr->get_product_price( $mock_product ); - - // Restore the sign-up fee after the test. - WC_Subscriptions_Product::set_sign_up_fee( 0 ); - - remove_all_filters( 'test_deposit_get_product' ); - } - - private function create_mock_subscription( $type ) { - $mock_product = $this->createMock( WC_Subscriptions_Product::class ); - - $mock_product - ->expects( $this->once() ) - ->method( 'get_price' ) - ->willReturn( 10 ); - - $mock_product - ->expects( $this->once() ) - ->method( 'get_type' ) - ->willReturn( $type ); - - return $mock_product; - } - - /** - * @dataProvider provide_get_product_tax_tests - */ - public function test_get_product_data_returns_the_same_as_build_display_items_without_shipping( $tax_enabled, $prices_include_tax, $tax_display_shop, $tax_display_cart, $_product_price, $_expected_price ) { - add_filter( 'wc_tax_enabled', $tax_enabled ? '__return_true' : '__return_false' ); // reset in tear_down. - add_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, "__return_$prices_include_tax" ] ); // reset in tear_down. - add_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, "__return_$tax_display_shop" ] ); // reset in tear_down. - add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, "__return_$tax_display_cart" ] ); // reset in tear_down. - add_filter( 'wc_shipping_enabled', '__return_false' ); // reset in tear_down. - WC()->cart->calculate_totals(); - $build_display_items_result = $this->express_checkout_helper->build_display_items( true ); - - $this->express_checkout_helper - ->method( 'is_product' ) - ->willReturn( true ); - - $this->express_checkout_helper - ->method( 'get_product' ) - ->willReturn( $this->simple_product ); - - $get_product_data_result = $this->pr->get_product_data(); - - foreach ( $get_product_data_result['displayItems'] as $key => $display_item ) { - if ( isset( $display_item['pending'] ) ) { - unset( $get_product_data_result['displayItems'][ $key ]['pending'] ); - } - } - - $this->assertEquals( - $get_product_data_result['displayItems'], - $build_display_items_result['displayItems'], - 'Failed asserting displayItems are the same for get_product_data and build_display_items' - ); - $this->assertEquals( - $get_product_data_result['total']['amount'], - $build_display_items_result['total']['amount'], - 'Failed asserting total amount are the same for get_product_data and build_display_items' - ); - } - - public function test_filter_cart_needs_shipping_address_returns_false() { - sleep( 1 ); - $this->zone->delete_shipping_method( $this->flat_rate_id ); - $this->zone->delete_shipping_method( $this->local_pickup_id ); - - WC_Subscriptions_Cart::set_cart_contains_subscription( true ); - - $this->assertFalse( $this->pr->filter_cart_needs_shipping_address( true ) ); - } - - public function test_filter_cart_needs_shipping_address_returns_true() { - WC_Subscriptions_Cart::set_cart_contains_subscription( true ); - - $this->assertTrue( $this->pr->filter_cart_needs_shipping_address( true ) ); - } - - public function test_get_button_settings() { - $this->express_checkout_helper - ->method( 'is_product' ) - ->willReturn( true ); - - $this->assertEquals( - [ - 'type' => 'default', - 'theme' => 'dark', - 'height' => '48', - 'locale' => 'en', - 'branded_type' => 'short', - 'radius' => '', - ], - $this->pr->get_button_settings() - ); - } - - public function test_filter_gateway_title() { - $order = $this->createMock( WC_Order::class ); - $order->method( 'get_payment_method_title' )->willReturn( 'Apple Pay' ); - - global $theorder; - $theorder = $order; - - $this->set_is_admin( true ); - $this->assertEquals( 'Apple Pay', $this->pr->filter_gateway_title( 'Original Title', 'woocommerce_payments' ) ); - - $this->set_is_admin( false ); - $this->assertEquals( 'Original Title', $this->pr->filter_gateway_title( 'Original Title', 'woocommerce_payments' ) ); - - $this->set_is_admin( true ); - $this->assertEquals( 'Original Title', $this->pr->filter_gateway_title( 'Original Title', 'another_gateway' ) ); - } - - /** - * @param bool $is_admin - */ - private function set_is_admin( bool $is_admin ) { - global $current_screen; - - if ( ! $is_admin ) { - $current_screen = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited - return; - } - - // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited - $current_screen = $this->getMockBuilder( \stdClass::class ) - ->setMethods( [ 'in_admin' ] ) - ->getMock(); - - $current_screen->method( 'in_admin' )->willReturn( $is_admin ); - } -} From 1f857c7e2212110f317502e48c91af8d0c730352 Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:12:36 -0600 Subject: [PATCH 15/26] Skip mysqlcheck SSL Requirement during E2E environment setup (#9941) --- bin/docker-setup.sh | 4 ++-- changelog/fix-skip-ssl-requirement-env-setup | 4 ++++ tests/e2e/env/setup.sh | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-skip-ssl-requirement-env-setup diff --git a/bin/docker-setup.sh b/bin/docker-setup.sh index 66c9fa1ee8b..6db41510908 100755 --- a/bin/docker-setup.sh +++ b/bin/docker-setup.sh @@ -27,11 +27,11 @@ cli() set +e # Wait for containers to be started up before the setup. # The db being accessible means that the db container started and the WP has been downloaded and the plugin linked -cli wp db check --path=/var/www/html --quiet > /dev/null +cli wp db check --skip_ssl --path=/var/www/html --quiet > /dev/null while [[ $? -ne 0 ]]; do echo "Waiting until the service is ready..." sleep 5 - cli wp db check --path=/var/www/html --quiet > /dev/null + cli wp db check --skip_ssl --path=/var/www/html --quiet > /dev/null done # If the plugin is already active then return early diff --git a/changelog/fix-skip-ssl-requirement-env-setup b/changelog/fix-skip-ssl-requirement-env-setup new file mode 100644 index 00000000000..691f98adbfa --- /dev/null +++ b/changelog/fix-skip-ssl-requirement-env-setup @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Skip mysqlcheck SSL Requirement during E2E environment setup diff --git a/tests/e2e/env/setup.sh b/tests/e2e/env/setup.sh index d2aa3a50e89..5ab08183bac 100755 --- a/tests/e2e/env/setup.sh +++ b/tests/e2e/env/setup.sh @@ -123,11 +123,11 @@ step "Setting up CLIENT site" # Wait for containers to be started up before the setup. # The db being accessible means that the db container started and the WP has been downloaded and the plugin linked set +e -cli wp db check --path=/var/www/html --quiet > /dev/null +cli wp db check --skip_ssl --path=/var/www/html --quiet > /dev/null while [[ $? -ne 0 ]]; do echo "Waiting until the service is ready..." sleep 5 - cli wp db check --path=/var/www/html --quiet > /dev/null + cli wp db check --skip_ssl --path=/var/www/html --quiet > /dev/null done echo "Client DB is up and running..." set -e From aa3203c358bb19c072fb48551394176fa6c2ef4e Mon Sep 17 00:00:00 2001 From: Nagesh Pai <4162931+nagpai@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:02:51 +0530 Subject: [PATCH 16/26] Reports: Update Transaction ID column label (#9911) Co-authored-by: Nagesh Pai Co-authored-by: Rua Haszard --- changelog/update-9910-transaction-id-label | 5 +++++ client/transactions/list/index.tsx | 2 +- client/transactions/list/test/index.tsx | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog/update-9910-transaction-id-label diff --git a/changelog/update-9910-transaction-id-label b/changelog/update-9910-transaction-id-label new file mode 100644 index 00000000000..0e43652d02b --- /dev/null +++ b/changelog/update-9910-transaction-id-label @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Change ID to uppercase in the 'Transaction ID' column label for consistency with similar unique IDs in the UI. + + diff --git a/client/transactions/list/index.tsx b/client/transactions/list/index.tsx index a8c437d6d2c..a5206ae0e5e 100644 --- a/client/transactions/list/index.tsx +++ b/client/transactions/list/index.tsx @@ -151,7 +151,7 @@ const getColumns = ( [ { key: 'transaction_id', - label: __( 'Transaction Id', 'woocommerce-payments' ), + label: __( 'Transaction ID', 'woocommerce-payments' ), visible: false, isLeftAligned: true, }, diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx index b2cf7f56664..b233b4d5477 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -621,7 +621,7 @@ describe( 'Transactions list', () => { getByRole( 'button', { name: 'Download' } ).click(); const expected = [ - '"Transaction Id"', + '"Transaction ID"', '"Date / Time (UTC)"', 'Type', 'Channel', From 6a67203525f66a592f284d5e05849b455f99799d Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Fri, 13 Dec 2024 13:44:44 +0100 Subject: [PATCH 17/26] Fix issue where order status and data are not updated when an order is in the processing state and being captured. (#9922) --- ...ndle-error-on-refund-during-manual-capture | 4 +++ includes/class-wc-payment-gateway-wcpay.php | 2 +- includes/class-wc-payments-order-service.php | 15 ++++++++++ .../test-class-wc-payments-order-service.php | 29 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-5671-handle-error-on-refund-during-manual-capture diff --git a/changelog/fix-5671-handle-error-on-refund-during-manual-capture b/changelog/fix-5671-handle-error-on-refund-during-manual-capture new file mode 100644 index 00000000000..016c68f13aa --- /dev/null +++ b/changelog/fix-5671-handle-error-on-refund-during-manual-capture @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed an issue where order metadata was not updated when capturing an order in the processing state. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 0f53c3dde42..cff7022a1b9 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -3373,7 +3373,7 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata $this->attach_exchange_info_to_order( $order, $charge_id ); if ( Intent_Status::SUCCEEDED === $status ) { - $this->order_service->update_order_status_from_intent( $order, $intent ); + $this->order_service->process_captured_payment( $order, $intent ); } elseif ( $is_authorization_expired ) { $this->order_service->mark_payment_capture_expired( $order, $intent_id, Intent_Status::CANCELED, $charge_id ); } else { diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php index c563877e830..f685b50debf 100644 --- a/includes/class-wc-payments-order-service.php +++ b/includes/class-wc-payments-order-service.php @@ -190,6 +190,21 @@ public function update_order_status_from_intent( $order, $intent ) { $this->complete_order_processing( $order ); } + /** + * Handles the order state when a payment is captured successfully. + * Unlike `update_order_status_from_intent`, this method does not check the current order status or skip processing + * if the order is already in the "processing" state. This ensures the order status is updated correctly upon a + * successful capture, preventing issues where the capture is not reflected in the order details or transaction screens + * due to the order status being in the processing state. + * + * @param WC_Order $order The order to update. + * @param WC_Payments_API_Abstract_Intention $intent The intent object containing payment or setup data. + */ + public function process_captured_payment( $order, $intent ) { + $this->mark_payment_capture_completed( $order, $intent ); + $this->complete_order_processing( $order, $intent->get_status() ); + } + /** * Updates an order to failed status, while adding a note with a link to the transaction. * diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php index 2eeaa50864e..d3fef27ea1f 100644 --- a/tests/unit/test-class-wc-payments-order-service.php +++ b/tests/unit/test-class-wc-payments-order-service.php @@ -1382,4 +1382,33 @@ public function test_add_note_and_metadata_for_refund_partially_refunded(): void WC_Helper_Order::delete_order( $order->get_id() ); } + + public function test_process_captured_payment() { + $order = WC_Helper_Order::create_order(); + $order->save(); + + $intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::SUCCEEDED ] ); + $this->order_service->set_intention_status_for_order( $this->order, Intent_Status::REQUIRES_CAPTURE ); + $this->order_service->set_intent_id_for_order( $order, $intent->get_id() ); + $order->set_status( Order_Status::PROCESSING ); // Let's simulate that order is set to processing, so order status should not interfere with the process. + $order->save(); + + $this->order_service->process_captured_payment( $order, $intent ); + + $this->assertEquals( $intent->get_status(), $this->order_service->get_intention_status_for_order( $order ) ); + + $this->assertTrue( $order->has_status( wc_get_is_paid_statuses() ) ); + + $notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); + $this->assertStringContainsString( 'successfully captured
using WooPayments', $notes[0]->content ); + $this->assertStringContainsString( '/payments/transactions/details&id=pi_mock" target="_blank" rel="noopener noreferrer">pi_mock', $notes[0]->content ); + + // Assert: Check that the order was unlocked. + $this->assertFalse( get_transient( 'wcpay_processing_intent_' . $order->get_id() ) ); + + // Assert: Applying the same data multiple times does not cause duplicate actions. + $this->order_service->update_order_status_from_intent( $order, $intent ); + $notes_2 = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); + $this->assertEquals( count( $notes ), count( $notes_2 ) ); + } } From 42bf30bc949206b42147e3565253ab8d8514239b Mon Sep 17 00:00:00 2001 From: Dan Paun <82826872+dpaun1985@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:17:16 +0200 Subject: [PATCH 18/26] Migrate capabilities from test-drive to live account (#9928) Co-authored-by: Dan Paun --- .../add-6924-migrate-test-drive-capabilities | 4 ++ includes/class-wc-payments-account.php | 47 +++++++++++++++++-- .../class-wc-payments-onboarding-service.php | 24 ++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 changelog/add-6924-migrate-test-drive-capabilities diff --git a/changelog/add-6924-migrate-test-drive-capabilities b/changelog/add-6924-migrate-test-drive-capabilities new file mode 100644 index 00000000000..7b280af4d92 --- /dev/null +++ b/changelog/add-6924-migrate-test-drive-capabilities @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Migrate active capabilities from test-drive account when switching to live account. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 85b0f336263..611c3a8acd2 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -30,6 +30,7 @@ class WC_Payments_Account implements MultiCurrencyAccountInterface { const ONBOARDING_STARTED_TRANSIENT = 'wcpay_on_boarding_started'; const ONBOARDING_STATE_TRANSIENT = 'wcpay_stripe_onboarding_state'; const WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT = 'woopay_enabled_by_default'; + const ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT = 'test_drive_account_settings_for_live_account'; const EMBEDDED_KYC_IN_PROGRESS_OPTION = 'wcpay_onboarding_embedded_kyc_in_progress'; const ERROR_MESSAGE_TRANSIENT = 'wcpay_error_message'; const INSTANT_DEPOSITS_REMINDER_ACTION = 'wcpay_instant_deposit_reminder'; @@ -1317,6 +1318,7 @@ public function maybe_handle_onboarding() { } $this->cleanup_on_account_reset(); + delete_transient( self::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ); // When we reset the account and want to go back to the settings page - redirect immediately! if ( $redirect_to_settings_page ) { @@ -1342,6 +1344,10 @@ public function maybe_handle_onboarding() { // in the "everything OK" scenario). if ( WC_Payments_Onboarding_Service::is_test_mode_enabled() ) { try { + // If we're in test mode and dealing with a test-drive account, + // we need to collect the test drive settings before we delete the test-drive account, + // and apply those settings to the live account. + $this->save_test_drive_settings(); // Delete the currently connected Stripe account. $this->payments_api_client->delete_account( true ); } catch ( API_Exception $e ) { @@ -1426,7 +1432,6 @@ public function maybe_handle_onboarding() { if ( ! $collect_payout_requirements && $this->has_working_jetpack_connection() && $this->is_stripe_account_valid() ) { - $params = [ 'source' => $onboarding_source, // Carry over some parameters as they may be used by our frontend logic. @@ -2149,13 +2154,11 @@ private function finalize_connection( string $state, string $mode, array $additi // If we get this parameter, but we have a valid state, it means the merchant left KYC early and didn't finish it. // While we do have an account, it is not yet valid. We need to redirect them back to the connect page. $params['wcpay-connection-error'] = '1'; - $this->redirect_service->redirect_to_connect_page( '', WC_Payments_Onboarding_Service::FROM_STRIPE, $params ); return; } $params['wcpay-connection-success'] = '1'; - $this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_STRIPE, $params ); } @@ -2582,4 +2585,42 @@ public function get_lifetime_total_payment_volume(): int { $account = $this->get_cached_account_data(); return (int) ! empty( $account ) && isset( $account['lifetime_total_payment_volume'] ) ? $account['lifetime_total_payment_volume'] : 0; } + + /** + * Extract the test drive settings from the account data that we want to store for the live account. + * ATM we only store the enabled payment methods. + * + * @return array The test drive settings for the live account. + */ + private function get_test_drive_settings_for_live_account(): array { + $gateway = WC_Payments::get_gateway(); + + $capabilities = []; + foreach ( $gateway->get_upe_enabled_payment_method_ids() as $payment_method_id ) { + $capabilities[ $payment_method_id . '_payments' ] = [ 'requested' => 'true' ]; + } + + return [ 'capabilities' => $capabilities ]; + } + + /** + * If we're in test mode and dealing with a test-drive account, + * we need to collect the test drive settings before we delete the test-drive account, + * and apply those settings to the live account. + * + * @return void + */ + private function save_test_drive_settings(): void { + $account = $this->get_cached_account_data(); + + if ( ! empty( $account['is_test_drive'] ) && true === $account['is_test_drive'] ) { + $test_drive_account_data = $this->get_test_drive_settings_for_live_account(); + + // Store the test drive settings for the live account in a transient, + // We don't passing the data around, as the merchant might cancel and start + // the onboarding from scratch. In this case, we won't have the test drive + // account anymore to collect the settings. + set_transient( self::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT, $test_drive_account_data, HOUR_IN_SECONDS ); + } + } } diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index bce87bcbf0c..ce558ac2faf 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -109,6 +109,7 @@ public function __construct( WC_Payments_API_Client $payments_api_client, Databa */ public function init_hooks() { add_filter( 'admin_body_class', [ $this, 'add_admin_body_classes' ] ); + add_filter( 'wc_payments_get_onboarding_data_args', [ $this, 'maybe_add_test_drive_settings_to_new_account_request' ] ); } /** @@ -902,4 +903,27 @@ public static function get_source( ?string $referer = null, ?array $get_params = // Default to an unknown source. return self::SOURCE_UNKNOWN; } + + /** + * If settings are collected from the test-drive account, + * include them in the existing arguments when creating the new account. + * + * @param array $args The request args to create new account. + * + * @return array The request args, possible updated with the test drive account settings, used to create new account. + */ + public function maybe_add_test_drive_settings_to_new_account_request( array $args ): array { + if ( + get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) && + is_array( get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) ) + ) { + $args['account_data'] = array_merge( + $args['account_data'], + get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) + ); + delete_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ); + } + + return $args; + } } From 1db617817604abba9ccb0ae092c81c1890414065 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 13 Dec 2024 16:13:52 +0100 Subject: [PATCH 19/26] Fix Jetpack onboarding by replacing the "from" query param (#9949) --- changelog/replace-from-url-query | 4 ++++ includes/wc-payment-api/class-wc-payments-http.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/replace-from-url-query diff --git a/changelog/replace-from-url-query b/changelog/replace-from-url-query new file mode 100644 index 00000000000..58688e1c42f --- /dev/null +++ b/changelog/replace-from-url-query @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix Jetpack onboarding URL query from "woocommerce-payments" to "woocommerce-core-profiler" diff --git a/includes/wc-payment-api/class-wc-payments-http.php b/includes/wc-payment-api/class-wc-payments-http.php index 658529190ed..1dc14048cfb 100644 --- a/includes/wc-payment-api/class-wc-payments-http.php +++ b/includes/wc-payment-api/class-wc-payments-http.php @@ -199,7 +199,7 @@ public function start_connection( $redirect ) { wp_safe_redirect( add_query_arg( [ - 'from' => 'woocommerce-payments', + 'from' => 'woocommerce-core-profiler', 'plugin_name' => 'woocommerce-payments', 'calypso_env' => $calypso_env, ], From 2e14de5dae812c3b02cf617fbac1448f71c29ed6 Mon Sep 17 00:00:00 2001 From: Igor Zinovyev Date: Fri, 13 Dec 2024 18:42:03 +0300 Subject: [PATCH 20/26] Added a callback instead of a direct call to avoid i18n notices. (#9884) Co-authored-by: Cvetan Cvetanov --- changelog/add-jetpack-config-callback | 4 + composer.json | 8 +- composer.lock | 379 +++++++++++++------------- woocommerce-payments.php | 8 +- 4 files changed, 206 insertions(+), 193 deletions(-) create mode 100644 changelog/add-jetpack-config-callback diff --git a/changelog/add-jetpack-config-callback b/changelog/add-jetpack-config-callback new file mode 100644 index 00000000000..64b1a2abb1b --- /dev/null +++ b/changelog/add-jetpack-config-callback @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Added conditional use of Jetpack Config callback to avoid i18n notices. diff --git a/composer.json b/composer.json index f5b03aaa04f..13f17e9c000 100644 --- a/composer.json +++ b/composer.json @@ -22,10 +22,10 @@ "require": { "php": ">=7.3", "ext-json": "*", - "automattic/jetpack-connection": "2.12.4", - "automattic/jetpack-config": "2.0.4", - "automattic/jetpack-autoloader": "3.0.10", - "automattic/jetpack-sync": "3.8.0", + "automattic/jetpack-connection": "6.2.0", + "automattic/jetpack-config": "3.0.0", + "automattic/jetpack-autoloader": "5.0.0", + "automattic/jetpack-sync": "4.1.0", "woocommerce/subscriptions-core": "6.7.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index f6276dc29e7..3e1d4ee08df 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f2c365c1ebb8b6af6e0df8c0ba64709", + "content-hash": "ed20d78f8b2b14b67df2266bd7614d62", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", - "version": "v2.0.4", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git", - "reference": "d1d726e4962d4bf6f9c51d01e63d613c3a9dd0bb" + "reference": "d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/d1d726e4962d4bf6f9c51d01e63d613c3a9dd0bb", - "reference": "d1d726e4962d4bf6f9c51d01e63d613c3a9dd0bb", + "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6", + "reference": "d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", + "automattic/jetpack-changelogger": "^5.0.0", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -34,11 +34,11 @@ "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-a8c-mc-stats", + "branch-alias": { + "dev-trunk": "3.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "2.0.x-dev" } }, "autoload": { @@ -52,31 +52,31 @@ ], "description": "Used to record internal usage stats for Automattic. Not visible to site owners.", "support": { - "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v2.0.4" + "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v3.0.0" }, - "time": "2024-11-04T09:23:35+00:00" + "time": "2024-11-14T20:12:50+00:00" }, { "name": "automattic/jetpack-admin-ui", - "version": "v0.4.6", + "version": "v0.5.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-admin-ui.git", - "reference": "a7bef1b075e35e431c0112f97763df9c6196ae39" + "reference": "a0894d34333451089add7b20f70e73b6509d6b6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/a7bef1b075e35e431c0112f97763df9c6196ae39", - "reference": "a7bef1b075e35e431c0112f97763df9c6196ae39", + "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/a0894d34333451089add7b20f70e73b6509d6b6d", + "reference": "a0894d34333451089add7b20f70e73b6509d6b6d", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "automattic/jetpack-logo": "^2.0.5", - "automattic/wordbless": "dev-master", + "automattic/jetpack-changelogger": "^5.1.0", + "automattic/jetpack-logo": "^3.0.0", + "automattic/wordbless": "^0.4.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -85,14 +85,14 @@ "type": "jetpack-library", "extra": { "autotagger": true, - "mirror-repo": "Automattic/jetpack-admin-ui", "textdomain": "jetpack-admin-ui", + "mirror-repo": "Automattic/jetpack-admin-ui", + "branch-alias": { + "dev-trunk": "0.5.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-admin-ui/compare/${old}...${new}" }, - "branch-alias": { - "dev-trunk": "0.4.x-dev" - }, "version-constants": { "::PACKAGE_VERSION": "src/class-admin-menu.php" } @@ -108,31 +108,31 @@ ], "description": "Generic Jetpack wp-admin UI elements", "support": { - "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.4.6" + "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.5.1" }, - "time": "2024-11-04T09:23:52+00:00" + "time": "2024-11-25T16:33:45+00:00" }, { "name": "automattic/jetpack-assets", - "version": "v2.3.13", + "version": "v4.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-assets.git", - "reference": "c520bffce576c823d7cbc851198201a820b7f981" + "reference": "ca1ebeceeeafb31876a234fa68ea3065b3eab2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/c520bffce576c823d7cbc851198201a820b7f981", - "reference": "c520bffce576c823d7cbc851198201a820b7f981", + "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/ca1ebeceeeafb31876a234fa68ea3065b3eab2c3", + "reference": "ca1ebeceeeafb31876a234fa68ea3065b3eab2c3", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2.0.5", - "php": ">=7.0" + "automattic/jetpack-constants": "^3.0.1", + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "brain/monkey": "2.6.1", + "automattic/jetpack-changelogger": "^5.1.0", + "brain/monkey": "^2.6.2", "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "^1.1.1" }, @@ -142,13 +142,13 @@ "type": "jetpack-library", "extra": { "autotagger": true, - "mirror-repo": "Automattic/jetpack-assets", "textdomain": "jetpack-assets", + "mirror-repo": "Automattic/jetpack-assets", + "branch-alias": { + "dev-trunk": "4.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-assets/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "2.3.x-dev" } }, "autoload": { @@ -165,46 +165,46 @@ ], "description": "Asset management utilities for Jetpack ecosystem packages", "support": { - "source": "https://github.com/Automattic/jetpack-assets/tree/v2.3.13" + "source": "https://github.com/Automattic/jetpack-assets/tree/v4.0.1" }, - "time": "2024-11-04T09:24:17+00:00" + "time": "2024-12-04T19:43:08+00:00" }, { "name": "automattic/jetpack-autoloader", - "version": "v3.0.10", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-autoloader.git", - "reference": "ec4c465ce6a47fb15c15ab0224ec5b1272422d3e" + "reference": "eb6331a5c50a03afd9896ce012e66858de9c49c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/ec4c465ce6a47fb15c15ab0224ec5b1272422d3e", - "reference": "ec4c465ce6a47fb15c15ab0224ec5b1272422d3e", + "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/eb6331a5c50a03afd9896ce012e66858de9c49c5", + "reference": "eb6331a5c50a03afd9896ce012e66858de9c49c5", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1 || ^2.0", - "php": ">=7.0" + "composer-plugin-api": "^2.2", + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.6", - "composer/composer": "^1.1 || ^2.0", + "automattic/jetpack-changelogger": "^5.1.0", + "composer/composer": "^2.2", "yoast/phpunit-polyfills": "^1.1.1" }, "type": "composer-plugin", "extra": { - "autotagger": true, "class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin", + "autotagger": true, "mirror-repo": "Automattic/jetpack-autoloader", + "branch-alias": { + "dev-trunk": "5.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" }, "version-constants": { "::VERSION": "src/AutoloadGenerator.php" - }, - "branch-alias": { - "dev-trunk": "3.0.x-dev" } }, "autoload": { @@ -229,29 +229,29 @@ "wordpress" ], "support": { - "source": "https://github.com/Automattic/jetpack-autoloader/tree/v3.0.10" + "source": "https://github.com/Automattic/jetpack-autoloader/tree/v5.0.0" }, - "time": "2024-08-26T14:49:14+00:00" + "time": "2024-11-25T16:33:57+00:00" }, { "name": "automattic/jetpack-config", - "version": "v2.0.4", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-config.git", - "reference": "9f075c81bae6fd638e0b3183612cda5cc9e01e06" + "reference": "fc719eff5073634b0c62793b05be913ca634e192" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/9f075c81bae6fd638e0b3183612cda5cc9e01e06", - "reference": "9f075c81bae6fd638e0b3183612cda5cc9e01e06", + "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/fc719eff5073634b0c62793b05be913ca634e192", + "reference": "fc719eff5073634b0c62793b05be913ca634e192", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.4", + "automattic/jetpack-changelogger": "^5.0.0", "automattic/jetpack-connection": "@dev", "automattic/jetpack-import": "@dev", "automattic/jetpack-jitm": "@dev", @@ -272,14 +272,14 @@ "type": "jetpack-library", "extra": { "autotagger": true, - "mirror-repo": "Automattic/jetpack-config", "textdomain": "jetpack-config", + "mirror-repo": "Automattic/jetpack-config", + "branch-alias": { + "dev-trunk": "3.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}" }, - "branch-alias": { - "dev-trunk": "2.0.x-dev" - }, "dependencies": { "test-only": [ "packages/connection", @@ -309,38 +309,38 @@ ], "description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.", "support": { - "source": "https://github.com/Automattic/jetpack-config/tree/v2.0.4" + "source": "https://github.com/Automattic/jetpack-config/tree/v3.0.0" }, - "time": "2024-06-24T19:22:07+00:00" + "time": "2024-11-14T20:12:40+00:00" }, { "name": "automattic/jetpack-connection", - "version": "v2.12.4", + "version": "v6.2.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-connection.git", - "reference": "35dd5b89b9936555ac185e83a489f41655974e70" + "reference": "52cd2ba7d845eb516d505959bd9a5e94d1bf4203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/35dd5b89b9936555ac185e83a489f41655974e70", - "reference": "35dd5b89b9936555ac185e83a489f41655974e70", + "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/52cd2ba7d845eb516d505959bd9a5e94d1bf4203", + "reference": "52cd2ba7d845eb516d505959bd9a5e94d1bf4203", "shasum": "" }, "require": { - "automattic/jetpack-a8c-mc-stats": "^2.0.2", - "automattic/jetpack-admin-ui": "^0.4.3", - "automattic/jetpack-assets": "^2.3.4", - "automattic/jetpack-constants": "^2.0.4", - "automattic/jetpack-redirect": "^2.0.3", - "automattic/jetpack-roles": "^2.0.3", - "automattic/jetpack-status": "^3.3.4", - "php": ">=7.0" + "automattic/jetpack-a8c-mc-stats": "^3.0.0", + "automattic/jetpack-admin-ui": "^0.5.1", + "automattic/jetpack-assets": "^4.0.1", + "automattic/jetpack-constants": "^3.0.1", + "automattic/jetpack-redirect": "^3.0.1", + "automattic/jetpack-roles": "^3.0.1", + "automattic/jetpack-status": "^5.0.1", + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.6", - "automattic/wordbless": "@dev", - "brain/monkey": "2.6.1", + "automattic/jetpack-changelogger": "^5.1.0", + "automattic/wordbless": "^0.4.2", + "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -349,25 +349,28 @@ "type": "jetpack-library", "extra": { "autotagger": true, - "mirror-repo": "Automattic/jetpack-connection", "textdomain": "jetpack-connection", - "version-constants": { - "::PACKAGE_VERSION": "src/class-package-version.php" + "mirror-repo": "Automattic/jetpack-connection", + "branch-alias": { + "dev-trunk": "6.2.x-dev" }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}" }, - "branch-alias": { - "dev-trunk": "2.12.x-dev" - }, "dependencies": { "test-only": [ "packages/licensing", "packages/sync" ] + }, + "version-constants": { + "::PACKAGE_VERSION": "src/class-package-version.php" } }, "autoload": { + "files": [ + "actions.php" + ], "classmap": [ "legacy", "src/", @@ -381,30 +384,30 @@ ], "description": "Everything needed to connect to the Jetpack infrastructure", "support": { - "source": "https://github.com/Automattic/jetpack-connection/tree/v2.12.4" + "source": "https://github.com/Automattic/jetpack-connection/tree/v6.2.0" }, - "time": "2024-08-23T14:29:32+00:00" + "time": "2024-12-09T15:47:56+00:00" }, { "name": "automattic/jetpack-constants", - "version": "v2.0.5", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", - "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1" + "reference": "d4b7820defcdb40c1add88d5ebd722e4ba80a873" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1", - "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/d4b7820defcdb40c1add88d5ebd722e4ba80a873", + "reference": "d4b7820defcdb40c1add88d5ebd722e4ba80a873", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "brain/monkey": "2.6.1", + "automattic/jetpack-changelogger": "^5.1.0", + "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -414,11 +417,11 @@ "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-constants", + "branch-alias": { + "dev-trunk": "3.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "2.0.x-dev" } }, "autoload": { @@ -432,30 +435,30 @@ ], "description": "A wrapper for defining constants in a more testable way.", "support": { - "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.5" + "source": "https://github.com/Automattic/jetpack-constants/tree/v3.0.1" }, - "time": "2024-11-04T09:23:35+00:00" + "time": "2024-11-25T16:33:27+00:00" }, { "name": "automattic/jetpack-ip", - "version": "v0.2.3", + "version": "v0.4.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-ip.git", - "reference": "f7a42b1603a24775c6f20eef2ac5cba3d6b37194" + "reference": "04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/f7a42b1603a24775c6f20eef2ac5cba3d6b37194", - "reference": "f7a42b1603a24775c6f20eef2ac5cba3d6b37194", + "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a", + "reference": "04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.6", - "brain/monkey": "2.6.1", + "automattic/jetpack-changelogger": "^5.1.0", + "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -464,14 +467,14 @@ "type": "jetpack-library", "extra": { "autotagger": true, + "textdomain": "jetpack-ip", "mirror-repo": "Automattic/jetpack-ip", + "branch-alias": { + "dev-trunk": "0.4.x-dev" + }, "changelogger": { "link-template": "https://github.com/automattic/jetpack-ip/compare/v${old}...v${new}" }, - "branch-alias": { - "dev-trunk": "0.2.x-dev" - }, - "textdomain": "jetpack-ip", "version-constants": { "::PACKAGE_VERSION": "src/class-utils.php" } @@ -487,30 +490,30 @@ ], "description": "Utilities for working with IP addresses.", "support": { - "source": "https://github.com/Automattic/jetpack-ip/tree/v0.2.3" + "source": "https://github.com/Automattic/jetpack-ip/tree/v0.4.1" }, - "time": "2024-08-23T14:28:05+00:00" + "time": "2024-11-25T16:33:22+00:00" }, { "name": "automattic/jetpack-password-checker", - "version": "v0.3.3", + "version": "v0.4.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-password-checker.git", - "reference": "1812a38452575e7c8c7c06affeeca776a367225f" + "reference": "e721e7659cc7a6a37152a4e96485e6c139f02d5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/1812a38452575e7c8c7c06affeeca776a367225f", - "reference": "1812a38452575e7c8c7c06affeeca776a367225f", + "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/e721e7659cc7a6a37152a4e96485e6c139f02d5f", + "reference": "e721e7659cc7a6a37152a4e96485e6c139f02d5f", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "automattic/wordbless": "@dev", + "automattic/jetpack-changelogger": "^5.1.0", + "automattic/wordbless": "^0.4.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -519,13 +522,13 @@ "type": "jetpack-library", "extra": { "autotagger": true, - "mirror-repo": "Automattic/jetpack-password-checker", "textdomain": "jetpack-password-checker", + "mirror-repo": "Automattic/jetpack-password-checker", + "branch-alias": { + "dev-trunk": "0.4.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-password-checker/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "0.3.x-dev" } }, "autoload": { @@ -539,31 +542,31 @@ ], "description": "Password Checker.", "support": { - "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.3.3" + "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.4.1" }, - "time": "2024-11-04T09:23:39+00:00" + "time": "2024-11-25T16:33:31+00:00" }, { "name": "automattic/jetpack-redirect", - "version": "v2.0.3", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-redirect.git", - "reference": "2c049bb08f736dc0dbafac7eaebea6f97cf8019e" + "reference": "89732a3ba1c5eba8cfd948b7567823cd884102d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/2c049bb08f736dc0dbafac7eaebea6f97cf8019e", - "reference": "2c049bb08f736dc0dbafac7eaebea6f97cf8019e", + "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/89732a3ba1c5eba8cfd948b7567823cd884102d5", + "reference": "89732a3ba1c5eba8cfd948b7567823cd884102d5", "shasum": "" }, "require": { - "automattic/jetpack-status": "^3.3.4", - "php": ">=7.0" + "automattic/jetpack-status": "^5.0.1", + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.6", - "brain/monkey": "2.6.1", + "automattic/jetpack-changelogger": "^5.1.0", + "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -573,11 +576,11 @@ "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-redirect", + "branch-alias": { + "dev-trunk": "3.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-redirect/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "2.0.x-dev" } }, "autoload": { @@ -591,30 +594,30 @@ ], "description": "Utilities to build URLs to the jetpack.com/redirect/ service", "support": { - "source": "https://github.com/Automattic/jetpack-redirect/tree/v2.0.3" + "source": "https://github.com/Automattic/jetpack-redirect/tree/v3.0.1" }, - "time": "2024-08-23T14:28:46+00:00" + "time": "2024-11-25T16:34:01+00:00" }, { "name": "automattic/jetpack-roles", - "version": "v2.0.4", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-roles.git", - "reference": "2fa5361ce8ff271cc4ecfac5be9b957ab0e9912f" + "reference": "fe5f2a45901ea14be00728119d097619615fb031" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/2fa5361ce8ff271cc4ecfac5be9b957ab0e9912f", - "reference": "2fa5361ce8ff271cc4ecfac5be9b957ab0e9912f", + "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/fe5f2a45901ea14be00728119d097619615fb031", + "reference": "fe5f2a45901ea14be00728119d097619615fb031", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "brain/monkey": "2.6.1", + "automattic/jetpack-changelogger": "^5.1.0", + "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -624,11 +627,11 @@ "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-roles", + "branch-alias": { + "dev-trunk": "3.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-roles/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "2.0.x-dev" } }, "autoload": { @@ -642,34 +645,34 @@ ], "description": "Utilities, related with user roles and capabilities.", "support": { - "source": "https://github.com/Automattic/jetpack-roles/tree/v2.0.4" + "source": "https://github.com/Automattic/jetpack-roles/tree/v3.0.1" }, - "time": "2024-11-04T09:23:38+00:00" + "time": "2024-11-25T16:33:29+00:00" }, { "name": "automattic/jetpack-status", - "version": "v3.3.5", + "version": "v5.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-status.git", - "reference": "69d5d8a8f31adf2b297a539bcddd9a9162d1320b" + "reference": "769f55b6327187a85c14ed21943eea430f63220d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/69d5d8a8f31adf2b297a539bcddd9a9162d1320b", - "reference": "69d5d8a8f31adf2b297a539bcddd9a9162d1320b", + "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/769f55b6327187a85c14ed21943eea430f63220d", + "reference": "769f55b6327187a85c14ed21943eea430f63220d", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2.0.4", - "php": ">=7.0" + "automattic/jetpack-constants": "^3.0.1", + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.6", + "automattic/jetpack-changelogger": "^5.1.0", "automattic/jetpack-connection": "@dev", - "automattic/jetpack-ip": "^0.2.3", + "automattic/jetpack-ip": "^0.4.1", "automattic/jetpack-plans": "@dev", - "brain/monkey": "2.6.1", + "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -679,12 +682,12 @@ "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-status", + "branch-alias": { + "dev-trunk": "5.0.x-dev" + }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}" }, - "branch-alias": { - "dev-trunk": "3.3.x-dev" - }, "dependencies": { "test-only": [ "packages/connection", @@ -703,38 +706,38 @@ ], "description": "Used to retrieve information about the current status of Jetpack and the site overall.", "support": { - "source": "https://github.com/Automattic/jetpack-status/tree/v3.3.5" + "source": "https://github.com/Automattic/jetpack-status/tree/v5.0.1" }, - "time": "2024-09-10T17:55:40+00:00" + "time": "2024-11-25T16:33:53+00:00" }, { "name": "automattic/jetpack-sync", - "version": "v3.8.0", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-sync.git", - "reference": "30b29f0c5a27e01cbf2fa592fbde97f617665153" + "reference": "5747f144575b9474622692f2bc8e4315363ea44d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/30b29f0c5a27e01cbf2fa592fbde97f617665153", - "reference": "30b29f0c5a27e01cbf2fa592fbde97f617665153", + "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/5747f144575b9474622692f2bc8e4315363ea44d", + "reference": "5747f144575b9474622692f2bc8e4315363ea44d", "shasum": "" }, "require": { - "automattic/jetpack-connection": "^2.12.4", - "automattic/jetpack-constants": "^2.0.4", - "automattic/jetpack-ip": "^0.2.3", - "automattic/jetpack-password-checker": "^0.3.2", - "automattic/jetpack-roles": "^2.0.3", - "automattic/jetpack-status": "^3.3.4", - "php": ">=7.0" + "automattic/jetpack-connection": "^6.2.0", + "automattic/jetpack-constants": "^3.0.1", + "automattic/jetpack-ip": "^0.4.1", + "automattic/jetpack-password-checker": "^0.4.1", + "automattic/jetpack-roles": "^3.0.1", + "automattic/jetpack-status": "^5.0.1", + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.6", + "automattic/jetpack-changelogger": "^5.1.0", "automattic/jetpack-search": "@dev", - "automattic/jetpack-waf": "^0.18.4", - "automattic/wordbless": "@dev", + "automattic/jetpack-waf": "^0.23.1", + "automattic/wordbless": "^0.4.2", "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { @@ -743,22 +746,22 @@ "type": "jetpack-library", "extra": { "autotagger": true, - "mirror-repo": "Automattic/jetpack-sync", "textdomain": "jetpack-sync", - "version-constants": { - "::PACKAGE_VERSION": "src/class-package-version.php" + "mirror-repo": "Automattic/jetpack-sync", + "branch-alias": { + "dev-trunk": "4.1.x-dev" }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-sync/compare/v${old}...v${new}" }, - "branch-alias": { - "dev-trunk": "3.8.x-dev" - }, "dependencies": { "test-only": [ "packages/search", "packages/waf" ] + }, + "version-constants": { + "::PACKAGE_VERSION": "src/class-package-version.php" } }, "autoload": { @@ -772,9 +775,9 @@ ], "description": "Everything needed to allow syncing to the WP.com infrastructure.", "support": { - "source": "https://github.com/Automattic/jetpack-sync/tree/v3.8.0" + "source": "https://github.com/Automattic/jetpack-sync/tree/v4.1.0" }, - "time": "2024-08-26T14:49:56+00:00" + "time": "2024-12-09T15:48:10+00:00" }, { "name": "composer/installers", diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 19012d26053..e69ae1934ad 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -78,6 +78,12 @@ function wcpay_jetpack_init() { if ( ! wcpay_check_old_jetpack_version() ) { return; } + $connection_version = Automattic\Jetpack\Connection\Package_Version::PACKAGE_VERSION; + + $custom_content = version_compare( $connection_version, '6.1.0', '>' ) ? + 'wcpay_get_jetpack_idc_custom_content' : + wcpay_get_jetpack_idc_custom_content(); + $jetpack_config = new Automattic\Jetpack\Config(); $jetpack_config->ensure( 'connection', @@ -90,7 +96,7 @@ function wcpay_jetpack_init() { 'identity_crisis', [ 'slug' => 'woocommerce-payments', - 'customContent' => wcpay_get_jetpack_idc_custom_content(), + 'customContent' => $custom_content, 'logo' => plugins_url( 'assets/images/logo.svg', WCPAY_PLUGIN_FILE ), 'admin_page' => '/wp-admin/admin.php?page=wc-admin', 'priority' => 5, From 4608ebb2317244de7bad6fc92fc94179be8547f5 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Fri, 13 Dec 2024 18:23:10 +0200 Subject: [PATCH 21/26] Compatibility Fix: Do not load translations early (#9940) Co-authored-by: Igor Zinovyev Co-authored-by: Cvetan Cvetanov --- .../compat-9727-avoid-early-translations | 4 + includes/admin/class-wc-payments-admin.php | 86 ++++++++--------- includes/class-wc-payment-gateway-wcpay.php | 96 +++++++++++-------- .../class-affirm-payment-method.php | 13 ++- .../class-afterpay-payment-method.php | 1 - .../class-cc-payment-method.php | 3 +- .../class-klarna-payment-method.php | 13 ++- .../class-link-payment-method.php | 13 ++- .../admin/test-class-wc-payments-admin.php | 2 + 9 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 changelog/compat-9727-avoid-early-translations diff --git a/changelog/compat-9727-avoid-early-translations b/changelog/compat-9727-avoid-early-translations new file mode 100644 index 00000000000..51432b8cd10 --- /dev/null +++ b/changelog/compat-9727-avoid-early-translations @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Remove translations during initialization, preventing unnecessary warnings. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 01cce6c775e..6509cc90ecb 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -144,49 +144,6 @@ public function __construct( $this->incentives_service = $incentives_service; $this->fraud_service = $fraud_service; $this->database_cache = $database_cache; - - $this->admin_child_pages = [ - 'wc-payments-overview' => [ - 'id' => 'wc-payments-overview', - 'title' => __( 'Overview', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/overview', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 10, - ], - ], - 'wc-payments-deposits' => [ - 'id' => 'wc-payments-deposits', - 'title' => __( 'Payouts', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/payouts', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 20, - ], - ], - 'wc-payments-transactions' => [ - 'id' => 'wc-payments-transactions', - 'title' => __( 'Transactions', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/transactions', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 30, - ], - ], - 'wc-payments-disputes' => [ - 'id' => 'wc-payments-disputes', - 'title' => __( 'Disputes', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/disputes', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 40, - ], - ], - ]; } /** @@ -315,6 +272,49 @@ public function add_payments_menu() { } global $submenu; + $this->admin_child_pages = [ + 'wc-payments-overview' => [ + 'id' => 'wc-payments-overview', + 'title' => __( 'Overview', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/overview', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 10, + ], + ], + 'wc-payments-deposits' => [ + 'id' => 'wc-payments-deposits', + 'title' => __( 'Payouts', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/payouts', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 20, + ], + ], + 'wc-payments-transactions' => [ + 'id' => 'wc-payments-transactions', + 'title' => __( 'Transactions', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/transactions', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 30, + ], + ], + 'wc-payments-disputes' => [ + 'id' => 'wc-payments-disputes', + 'title' => __( 'Disputes', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/disputes', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 40, + ], + ], + ]; + try { // Render full payments menu with sub-items only if: // - we have working WPCOM/Jetpack connection; diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index cff7022a1b9..3e02aa6acf5 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -309,13 +309,11 @@ public function __construct( $this->fraud_service = $fraud_service; $this->duplicate_payment_methods_detection_service = $duplicate_payment_methods_detection_service; - $this->id = static::GATEWAY_ID; - $this->icon = $this->get_theme_icon(); - $this->has_fields = true; - $this->method_title = 'WooPayments'; - $this->method_description = $this->get_method_description(); + $this->id = static::GATEWAY_ID; + $this->icon = $this->get_theme_icon(); + $this->has_fields = true; + $this->method_title = 'WooPayments'; - $this->title = $payment_method->get_title(); $this->description = ''; $this->supports = [ 'products', @@ -327,7 +325,57 @@ public function __construct( $this->method_title = "WooPayments ($this->title)"; } - // Define setting fields. + // Capabilities have different keys than the payment method ID's, + // so instead of appending '_payments' to the end of the ID, it'll be better + // to have a map for it instead, just in case the pattern changes. + $this->payment_method_capability_key_map = [ + 'sofort' => 'sofort_payments', + 'giropay' => 'giropay_payments', + 'bancontact' => 'bancontact_payments', + 'eps' => 'eps_payments', + 'ideal' => 'ideal_payments', + 'p24' => 'p24_payments', + 'card' => 'card_payments', + 'sepa_debit' => 'sepa_debit_payments', + 'au_becs_debit' => 'au_becs_debit_payments', + 'link' => 'link_payments', + 'affirm' => 'affirm_payments', + 'afterpay_clearpay' => 'afterpay_clearpay_payments', + 'klarna' => 'klarna_payments', + 'jcb' => 'jcb_payments', + ]; + + // WooPay utilities. + $this->woopay_util = new WooPay_Utilities(); + + // Load the settings. + $this->init_settings(); + + // Check if subscriptions are enabled and add support for them. + $this->maybe_init_subscriptions(); + + // If the setting to enable saved cards is enabled, then we should support tokenization and adding payment methods. + if ( $this->is_saved_cards_enabled() ) { + array_push( $this->supports, 'tokenization', 'add_payment_method' ); + } + } + + /** + * Return the gateway's title. + * + * @return string + */ + public function get_title() { + $this->title = $this->payment_method->get_title(); + return parent::get_title(); + } + + /** + * Get the form fields after they are initialized. + * + * @return array of options + */ + public function get_form_fields() { $this->form_fields = [ 'enabled' => [ 'title' => __( 'Enable/disable', 'woocommerce-payments' ), @@ -497,39 +545,7 @@ public function __construct( 'platform_checkout_custom_message' => [ 'default' => __( 'By placing this order, you agree to our [terms] and understand our [privacy_policy].', 'woocommerce-payments' ) ], ]; - // Capabilities have different keys than the payment method ID's, - // so instead of appending '_payments' to the end of the ID, it'll be better - // to have a map for it instead, just in case the pattern changes. - $this->payment_method_capability_key_map = [ - 'sofort' => 'sofort_payments', - 'giropay' => 'giropay_payments', - 'bancontact' => 'bancontact_payments', - 'eps' => 'eps_payments', - 'ideal' => 'ideal_payments', - 'p24' => 'p24_payments', - 'card' => 'card_payments', - 'sepa_debit' => 'sepa_debit_payments', - 'au_becs_debit' => 'au_becs_debit_payments', - 'link' => 'link_payments', - 'affirm' => 'affirm_payments', - 'afterpay_clearpay' => 'afterpay_clearpay_payments', - 'klarna' => 'klarna_payments', - 'jcb' => 'jcb_payments', - ]; - - // WooPay utilities. - $this->woopay_util = new WooPay_Utilities(); - - // Load the settings. - $this->init_settings(); - - // Check if subscriptions are enabled and add support for them. - $this->maybe_init_subscriptions(); - - // If the setting to enable saved cards is enabled, then we should support tokenization and adding payment methods. - if ( $this->is_saved_cards_enabled() ) { - array_push( $this->supports, 'tokenization', 'add_payment_method' ); - } + return parent::get_form_fields(); } /** diff --git a/includes/payment-methods/class-affirm-payment-method.php b/includes/payment-methods/class-affirm-payment-method.php index 47b89f49951..1c87c67149f 100644 --- a/includes/payment-methods/class-affirm-payment-method.php +++ b/includes/payment-methods/class-affirm-payment-method.php @@ -27,7 +27,6 @@ class Affirm_Payment_Method extends UPE_Payment_Method { public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; - $this->title = __( 'Affirm', 'woocommerce-payments' ); $this->is_reusable = false; $this->is_bnpl = true; $this->icon_url = plugins_url( 'assets/images/payment-methods/affirm-logo.svg', WCPAY_PLUGIN_FILE ); @@ -38,6 +37,18 @@ public function __construct( $token_service ) { $this->countries = [ Country_Code::UNITED_STATES, Country_Code::CANADA ]; } + /** + * Returns payment method title + * + * @param string|null $account_country Country of merchants account. + * @param array|false $payment_details Optional payment details from charge object. + * + * @return string + */ + public function get_title( ?string $account_country = null, $payment_details = false ) { + return __( 'Affirm', 'woocommerce-payments' ); + } + /** * Returns testing credentials to be printed at checkout in test mode. * diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php index 3674731835c..503f0c6104d 100644 --- a/includes/payment-methods/class-afterpay-payment-method.php +++ b/includes/payment-methods/class-afterpay-payment-method.php @@ -27,7 +27,6 @@ class Afterpay_Payment_Method extends UPE_Payment_Method { public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; - $this->title = __( 'Afterpay', 'woocommerce-payments' ); $this->is_reusable = false; $this->is_bnpl = true; $this->icon_url = plugins_url( 'assets/images/payment-methods/afterpay-logo.svg', WCPAY_PLUGIN_FILE ); diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php index 50a44fa1114..58d7d733a77 100644 --- a/includes/payment-methods/class-cc-payment-method.php +++ b/includes/payment-methods/class-cc-payment-method.php @@ -25,7 +25,6 @@ class CC_Payment_Method extends UPE_Payment_Method { public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; - $this->title = __( 'Credit card / debit card', 'woocommerce-payments' ); $this->is_reusable = true; $this->currencies = [];// All currencies are supported. $this->icon_url = plugins_url( 'assets/images/payment-methods/generic-card.svg', WCPAY_PLUGIN_FILE ); @@ -40,7 +39,7 @@ public function __construct( $token_service ) { */ public function get_title( ?string $account_country = null, $payment_details = false ) { if ( ! $payment_details ) { - return $this->title; + return __( 'Credit card / debit card', 'woocommerce-payments' ); } $details = $payment_details[ $this->stripe_id ]; diff --git a/includes/payment-methods/class-klarna-payment-method.php b/includes/payment-methods/class-klarna-payment-method.php index 31c71cb813a..27495db4b02 100644 --- a/includes/payment-methods/class-klarna-payment-method.php +++ b/includes/payment-methods/class-klarna-payment-method.php @@ -27,7 +27,6 @@ class Klarna_Payment_Method extends UPE_Payment_Method { public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; - $this->title = __( 'Klarna', 'woocommerce-payments' ); $this->is_reusable = false; $this->is_bnpl = true; $this->icon_url = plugins_url( 'assets/images/payment-methods/klarna-pill.svg', WCPAY_PLUGIN_FILE ); @@ -37,6 +36,18 @@ public function __construct( $token_service ) { $this->limits_per_currency = WC_Payments_Utils::get_bnpl_limits_per_currency( self::PAYMENT_METHOD_STRIPE_ID ); } + /** + * Returns payment method title + * + * @param string|null $account_country Country of merchants account. + * @param array|false $payment_details Optional payment details from charge object. + * + * @return string + */ + public function get_title( ?string $account_country = null, $payment_details = false ) { + return __( 'Klarna', 'woocommerce-payments' ); + } + /** * Returns payment method supported countries. * diff --git a/includes/payment-methods/class-link-payment-method.php b/includes/payment-methods/class-link-payment-method.php index c5c189bbad8..0e086cd7e86 100644 --- a/includes/payment-methods/class-link-payment-method.php +++ b/includes/payment-methods/class-link-payment-method.php @@ -25,12 +25,23 @@ class Link_Payment_Method extends UPE_Payment_Method { public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; - $this->title = __( 'Link', 'woocommerce-payments' ); $this->is_reusable = true; $this->currencies = [ Currency_Code::UNITED_STATES_DOLLAR ]; $this->icon_url = plugins_url( 'assets/images/payment-methods/link.svg', WCPAY_PLUGIN_FILE ); } + /** + * Returns payment method title + * + * @param string|null $account_country Country of merchants account. + * @param array|false $payment_details Optional payment details from charge object. + * + * @return string + */ + public function get_title( ?string $account_country = null, $payment_details = false ) { + return __( 'Link', 'woocommerce-payments' ); + } + /** * Returns testing credentials to be printed at checkout in test mode. * diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 6a16577f18c..6dba99d9d1b 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -203,6 +203,8 @@ private function mock_current_user_is_admin() { */ public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $has_working_jetpack_connection, $is_stripe_account_valid, $get_params ) { $this->mock_current_user_is_admin(); + $this->payments_admin->add_payments_menu(); + $_GET = $get_params; $this->mock_account From 5064ccbd23bfc9005fcb4c34405922577bf55aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:46:29 -0300 Subject: [PATCH 22/26] Add login confirmation check to ECE in Blocks (#9944) --- changelog/fix-9806-ECE-subscription-checkout-signed-out | 4 ++++ .../express-checkout/blocks/hooks/use-express-checkout.js | 7 +++++++ .../blocks/hooks/use-express-checkout.js | 7 +++++++ 3 files changed, 18 insertions(+) create mode 100644 changelog/fix-9806-ECE-subscription-checkout-signed-out diff --git a/changelog/fix-9806-ECE-subscription-checkout-signed-out b/changelog/fix-9806-ECE-subscription-checkout-signed-out new file mode 100644 index 00000000000..fa25afd1f10 --- /dev/null +++ b/changelog/fix-9806-ECE-subscription-checkout-signed-out @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Ensure ECE login confirmation dialog is shown on Blocks. diff --git a/client/express-checkout/blocks/hooks/use-express-checkout.js b/client/express-checkout/blocks/hooks/use-express-checkout.js index 962f74e5876..e2d68bc6bce 100644 --- a/client/express-checkout/blocks/hooks/use-express-checkout.js +++ b/client/express-checkout/blocks/hooks/use-express-checkout.js @@ -8,6 +8,7 @@ import { useStripe, useElements } from '@stripe/react-stripe-js'; * Internal dependencies */ import { + displayLoginConfirmation, getExpressCheckoutButtonStyleSettings, getExpressCheckoutData, normalizeLineItems, @@ -52,6 +53,12 @@ export const useExpressCheckout = ( { const onButtonClick = useCallback( ( event ) => { + // If login is required for checkout, display redirect confirmation dialog. + if ( getExpressCheckoutData( 'login_confirmation' ) ) { + displayLoginConfirmation( event.expressPaymentType ); + return; + } + const options = { lineItems: normalizeLineItems( billing?.cartTotalItems ), emailRequired: true, diff --git a/client/tokenized-express-checkout/blocks/hooks/use-express-checkout.js b/client/tokenized-express-checkout/blocks/hooks/use-express-checkout.js index f33604354f7..afdcca3f6d2 100644 --- a/client/tokenized-express-checkout/blocks/hooks/use-express-checkout.js +++ b/client/tokenized-express-checkout/blocks/hooks/use-express-checkout.js @@ -8,6 +8,7 @@ import { useStripe, useElements } from '@stripe/react-stripe-js'; * Internal dependencies */ import { + displayLoginConfirmation, getExpressCheckoutButtonStyleSettings, getExpressCheckoutData, normalizeLineItems, @@ -52,6 +53,12 @@ export const useExpressCheckout = ( { const onButtonClick = useCallback( ( event ) => { + // If login is required for checkout, display redirect confirmation dialog. + if ( getExpressCheckoutData( 'login_confirmation' ) ) { + displayLoginConfirmation( event.expressPaymentType ); + return; + } + const options = { lineItems: normalizeLineItems( billing?.cartTotalItems ), emailRequired: true, From 6537f031c85fa5a62ef41df52116eb1fce31d031 Mon Sep 17 00:00:00 2001 From: Francesco Date: Fri, 13 Dec 2024 18:30:14 +0100 Subject: [PATCH 23/26] fix: tokenized cart error notice json (#9950) --- .../fix-tokenized-cart-error-notice-json | 5 ++++ .../event-handlers.js | 23 ++++++++++++------- .../tokenized-express-checkout/utils/index.ts | 4 +++- 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 changelog/fix-tokenized-cart-error-notice-json diff --git a/changelog/fix-tokenized-cart-error-notice-json b/changelog/fix-tokenized-cart-error-notice-json new file mode 100644 index 00000000000..c132e0f7eeb --- /dev/null +++ b/changelog/fix-tokenized-cart-error-notice-json @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: fix: tokenized cart error notice json + + diff --git a/client/tokenized-express-checkout/event-handlers.js b/client/tokenized-express-checkout/event-handlers.js index cc300e36ef5..12ec2513cc7 100644 --- a/client/tokenized-express-checkout/event-handlers.js +++ b/client/tokenized-express-checkout/event-handlers.js @@ -149,16 +149,23 @@ export const onConfirmHandler = async ( completePayment( redirectUrl ); } } catch ( e ) { + // API errors are not parsed, so we need to do it ourselves. + if ( e.json ) { + e = e.json(); + } + return abortPayment( event, - getErrorMessageFromNotice( e.message ) || - e.payment_result?.payment_details.find( - ( detail ) => detail.key === 'errorMessage' - )?.value || - __( - 'There was a problem processing the order.', - 'woocommerce-payments' - ) + getErrorMessageFromNotice( + e.message || + e.payment_result?.payment_details.find( + ( detail ) => detail.key === 'errorMessage' + )?.value || + __( + 'There was a problem processing the order.', + 'woocommerce-payments' + ) + ) ); } }; diff --git a/client/tokenized-express-checkout/utils/index.ts b/client/tokenized-express-checkout/utils/index.ts index 9b92ec023ba..98ee8b90091 100644 --- a/client/tokenized-express-checkout/utils/index.ts +++ b/client/tokenized-express-checkout/utils/index.ts @@ -27,7 +27,9 @@ export const getExpressCheckoutData = < * @param notice Error notice. * @return Error messages. */ -export const getErrorMessageFromNotice = ( notice: string ) => { +export const getErrorMessageFromNotice = ( notice: string | undefined ) => { + if ( ! notice ) return ''; + const div = document.createElement( 'div' ); div.innerHTML = notice.trim(); return div.firstChild ? div.firstChild.textContent : ''; From 5e3b32e09b5379fb82a910852209438861700b6d Mon Sep 17 00:00:00 2001 From: Hector Lovo Date: Fri, 13 Dec 2024 16:28:18 -0500 Subject: [PATCH 24/26] =?UTF-8?q?Ensure=20WooPay=20=E2=80=98Enabled=20by?= =?UTF-8?q?=20Default=E2=80=99=20Value=20Is=20Correctly=20Set=20In=20Sandb?= =?UTF-8?q?ox=20Mode=20(#9898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ix-9421-auto-enable-woopay-in-sandbox-mode | 4 ++ client/connect-account-page/index.tsx | 18 +++-- includes/class-wc-payments-account.php | 15 ++-- tests/unit/test-class-wc-payments-account.php | 70 +++++++++++++++++++ 4 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 changelog/fix-9421-auto-enable-woopay-in-sandbox-mode diff --git a/changelog/fix-9421-auto-enable-woopay-in-sandbox-mode b/changelog/fix-9421-auto-enable-woopay-in-sandbox-mode new file mode 100644 index 00000000000..30ec0c7fed5 --- /dev/null +++ b/changelog/fix-9421-auto-enable-woopay-in-sandbox-mode @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Ensure WooPay 'enabled by default' value is correctly set in sandbox mode. diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index faa5d94311c..6502975d642 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -166,7 +166,7 @@ const ConnectAccountPage: React.FC = () => { } }; - const checkAccountStatus = () => { + const checkAccountStatus = ( extraQueryArgs = {} ) => { // Fetch account status from the cache. apiFetch( { path: `/wc/v3/payments/accounts`, @@ -188,18 +188,22 @@ const ConnectAccountPage: React.FC = () => { loaderProgressRef.current > 95 ) { setTestDriveLoaderProgress( 100 ); - - // Redirect to the Connect URL and let it figure it out where to point the merchant. - window.location.href = addQueryArgs( connectUrl, { + const queryArgs = { test_drive: 'true', 'wcpay-sandbox-success': 'true', source: determineTrackingSource(), from: 'WCPAY_CONNECT', redirect_to_settings_page: urlParams.get( 'redirect_to_settings_page' ) || '', + }; + + // Redirect to the Connect URL and let it figure it out where to point the merchant. + window.location.href = addQueryArgs( connectUrl, { + ...queryArgs, + ...extraQueryArgs, } ); } else { - setTimeout( checkAccountStatus, 2000 ); + setTimeout( () => checkAccountStatus( extraQueryArgs ), 2000 ); } } ); }; @@ -264,7 +268,9 @@ const ConnectAccountPage: React.FC = () => { // The account has been successfully onboarded. if ( !! connectionSuccess ) { // Start checking the account status in a loop. - checkAccountStatus(); + checkAccountStatus( { + 'wcpay-connection-success': '1', + } ); } else { // Redirect to the response URL, but attach our test drive flags. // This URL is generally a Connect page URL. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 611c3a8acd2..097ef468710 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -2009,9 +2009,19 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne $collect_payout_requirements ); + $should_enable_woopay = filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ); + $is_test_mode = in_array( $setup_mode, [ 'test', 'test_drive' ], true ); + $account_already_exists = isset( $onboarding_data['url'] ) && false === $onboarding_data['url']; + + // Only store the 'woopay_enabled_by_default' flag in a transient, to be enabled later, if + // it should be enabled and the account doesn't already exist, or we are in test mode. + if ( $should_enable_woopay && ( ! $account_already_exists || $is_test_mode ) ) { + set_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, $should_enable_woopay, DAY_IN_SECONDS ); + } + // If an account already exists for this site and/or there is no need for KYC verifications, we're done. // Our platform will respond with a `false` URL in this case. - if ( isset( $onboarding_data['url'] ) && false === $onboarding_data['url'] ) { + if ( $account_already_exists ) { // Set the gateway options. $gateway = WC_Payments::get_gateway(); $gateway->update_option( 'enabled', 'yes' ); @@ -2032,9 +2042,6 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne ); } - // We have an account that needs to be verified (has a URL to redirect the merchant to). - // Store the relevant onboarding data. - set_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ), DAY_IN_SECONDS ); // Save the onboarding state for a day. // This is used to verify the state when finalizing the onboarding and connecting the account. // On finalizing the onboarding, the transient gets deleted. diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index 934a7a72ada..d46a32722af 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -907,6 +907,76 @@ public function test_maybe_handle_onboarding_init_embedded_kyc() { $this->wcpay_account->maybe_handle_onboarding(); } + public function test_ensure_woopay_enabled_by_default_value_set_in_sandbox_mode_kyc() { + // Arrange. + // We need to be in the WP admin dashboard. + $this->set_is_admin( true ); + // Test as an admin user. + wp_set_current_user( 1 ); + + // Configure the request to be in sandbox mode. + $_GET['wcpay-connect'] = 'connect-from'; + $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' ); + $_GET['progressive'] = 'true'; + $_GET['test_mode'] = 'true'; + $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD; + + // The Jetpack connection is in working order. + $this->mock_jetpack_connection(); + + $this->mock_api_client + ->expects( $this->once() ) + ->method( 'get_onboarding_data' ) + ->willReturn( + [ + 'url' => false, + 'woopay_enabled_by_default' => true, + ] + ); + + $original_value = get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ); + + // Act. + $this->wcpay_account->maybe_handle_onboarding(); + + // Assert. + $this->assertFalse( $original_value ); + $this->assertTrue( get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ) ); + } + + public function test_ensure_woopay_not_enabled_by_default_for_existing_live_accounts() { + // Arrange. + // We need to be in the WP admin dashboard. + $this->set_is_admin( true ); + // Test as an admin user. + wp_set_current_user( 1 ); + + // Configure the request to be in sandbox mode. + $_GET['wcpay-connect'] = 'connect-from'; + $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' ); + $_GET['progressive'] = 'true'; + $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD; + + // The Jetpack connection is in working order. + $this->mock_jetpack_connection(); + + $this->mock_api_client + ->expects( $this->once() ) + ->method( 'get_onboarding_data' ) + ->willReturn( + [ + 'url' => false, + 'woopay_enabled_by_default' => true, + ] + ); + + // Act. + $this->wcpay_account->maybe_handle_onboarding(); + + // Assert. + $this->assertFalse( get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ) ); + } + public function test_maybe_handle_onboarding_init_stripe_onboarding_existing_account() { // Arrange. // We need to be in the WP admin dashboard. From 009bc4416b6a3d74efe68e79b7ae2c5195d43531 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 13 Dec 2024 17:27:25 -0500 Subject: [PATCH 25/26] Enable ECE for Virtual Variable Subscriptions with Free Trials (#9917) --- changelog/as-fix-ece-variable-subs-free-trial | 4 ++++ client/express-checkout/index.js | 23 +++++++++++++++---- client/express-checkout/utils/index.ts | 1 + ...payments-express-checkout-ajax-handler.php | 1 + ...ayments-express-checkout-button-helper.php | 5 +--- 5 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 changelog/as-fix-ece-variable-subs-free-trial diff --git a/changelog/as-fix-ece-variable-subs-free-trial b/changelog/as-fix-ece-variable-subs-free-trial new file mode 100644 index 00000000000..64d67393c06 --- /dev/null +++ b/changelog/as-fix-ece-variable-subs-free-trial @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Enable ECE for Virtual Variable Subscriptions with Free Trials. diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index 0e2239946b6..447f0c81198 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -363,7 +363,7 @@ jQuery( ( $ ) => { } ); if ( getExpressCheckoutData( 'button_context' ) === 'product' ) { - wcpayECE.attachProductPageEventListeners( elements ); + wcpayECE.attachProductPageEventListeners( elements, eceButton ); } }, @@ -414,7 +414,7 @@ jQuery( ( $ ) => { return api.expressCheckoutECEGetSelectedProductData( data ); }, - attachProductPageEventListeners: ( elements ) => { + attachProductPageEventListeners: ( elements, eceButton ) => { // WooCommerce Deposits support. // Trigger the "woocommerce_variation_has_changed" event when the deposit option is changed. // Needs to be defined before the `woocommerce_variation_has_changed` event handler is set. @@ -437,6 +437,18 @@ jQuery( ( $ ) => { $.when( wcpayECE.getSelectedProductData() ) .then( ( response ) => { + // We do not support variable subscriptions with variations + // that require shipping and include a free trial. + if ( + getExpressCheckoutData( 'product' ) + .product_type === 'variable-subscription' && + response.needs_shipping && + response.has_free_trial + ) { + eceButton.destroy(); + return; + } + const isDeposits = wcpayECE.productHasDepositOption(); /** * If the customer aborted the express checkout, @@ -449,8 +461,11 @@ jQuery( ( $ ) => { ! wcpayECE.paymentAborted && getExpressCheckoutData( 'product' ) .needs_shipping === response.needs_shipping; - - if ( ! isDeposits && needsShipping ) { + if ( + ! isDeposits && + needsShipping && + ! ( eceButton._destroyed ?? false ) + ) { elements.update( { amount: response.total.amount, } ); diff --git a/client/express-checkout/utils/index.ts b/client/express-checkout/utils/index.ts index cfbc2b25b2b..7e6c4bf2d09 100644 --- a/client/express-checkout/utils/index.ts +++ b/client/express-checkout/utils/index.ts @@ -66,6 +66,7 @@ export interface WCPayExpressCheckoutParams { product: { needs_shipping: boolean; currency: string; + product_type: string; shippingOptions: { id: string; label: string; diff --git a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php index bdf54ec01cf..d14460da71e 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php @@ -333,6 +333,7 @@ public function ajax_get_selected_product_data() { $data['needs_shipping'] = wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping(); $data['currency'] = strtolower( get_woocommerce_currency() ); $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); + $data['has_free_trial'] = class_exists( 'WC_Subscriptions_Product' ) ? WC_Subscriptions_Product::get_trial_length( $product ) > 0 : false; wp_send_json( $data ); } catch ( Exception $e ) { diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 26b72aa941c..672f2584c67 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -742,6 +742,7 @@ public function get_product_data() { $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ); $data['currency'] = strtolower( $currency ); $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); + $data['product_type'] = $product->get_type(); return apply_filters( 'wcpay_payment_request_product_data', $data, $product ); } @@ -767,13 +768,9 @@ private function is_product_supported() { // Simple subscription that needs shipping with free trials is not supported. $is_free_trial_simple_subs = class_exists( 'WC_Subscriptions_Product' ) && $product->get_type() === 'subscription' && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0; - // Disable ECE for all variable subscriptions with free trials, as they are not currently supported. For now, ECE will be disabled for all such cases, and a solution will be addressed in a separate PR. - $is_free_trial_variable_sub = class_exists( 'WC_Subscriptions_Product' ) && $product->get_type() === 'variable-subscription' && WC_Subscriptions_Product::get_trial_length( $product ) > 0; - if ( ! in_array( $product->get_type(), $this->supported_product_types(), true ) || $is_free_trial_simple_subs - || $is_free_trial_variable_sub || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported. || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page. || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page. From cb65e92cc9d3a263713a22998439e1cfad23d6ec Mon Sep 17 00:00:00 2001 From: Cvetan Cvetanov Date: Sun, 15 Dec 2024 21:15:46 +0200 Subject: [PATCH 26/26] Add support for utilizing NOX capabilities as URL parameters during account creation (#9947) Co-authored-by: oaratovskyi Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Co-authored-by: Vlad Olaru --- ...s-capabilities-to-onboarding-as-get-params | 4 + client/connect-account-page/index.tsx | 1 + client/onboarding/utils.ts | 2 + includes/class-wc-payments-account.php | 44 +++++-- .../class-wc-payments-onboarding-service.php | 108 +++++++++++++++++- 5 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 changelog/update-pass-capabilities-to-onboarding-as-get-params diff --git a/changelog/update-pass-capabilities-to-onboarding-as-get-params b/changelog/update-pass-capabilities-to-onboarding-as-get-params new file mode 100644 index 00000000000..9104e7a8f99 --- /dev/null +++ b/changelog/update-pass-capabilities-to-onboarding-as-get-params @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add support for utilizing NOX capabilities as URL parameters during account creation. diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 6502975d642..2b3f402abcb 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -215,6 +215,7 @@ const ConnectAccountPage: React.FC = () => { const customizedConnectUrl = addQueryArgs( connectUrl, { test_drive: 'true', + capabilities: urlParams.get( 'capabilities' ) || '', } ); const updateProgress = setInterval( updateLoaderProgress, 2500, 40, 5 ); diff --git a/client/onboarding/utils.ts b/client/onboarding/utils.ts index a95c3e298ef..306328f64f7 100644 --- a/client/onboarding/utils.ts +++ b/client/onboarding/utils.ts @@ -65,9 +65,11 @@ export const createAccountSession = async ( data: OnboardingFields, isPoEligible: boolean ): Promise< AccountKycSession > => { + const urlParams = new URLSearchParams( window.location.search ); return await apiFetch< AccountKycSession >( { path: addQueryArgs( `${ NAMESPACE }/onboarding/kyc/session`, { self_assessment: fromDotNotation( data ), + capabilities: urlParams.get( 'capabilities' ) || '', progressive: isPoEligible, } ), method: 'GET', diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 097ef468710..dc5430d6f0f 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -1497,7 +1497,7 @@ public function maybe_handle_onboarding() { // If there is a working one, we can proceed with the Stripe account handling. try { $this->maybe_init_jetpack_connection( - // Carry over all the important GET params, so we have them after the Jetpack connection setup. + // Carry over all the important GET params, so we have them after the Jetpack connection setup. add_query_arg( [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, @@ -1506,6 +1506,10 @@ public function maybe_handle_onboarding() { 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, 'test_drive' => $create_test_drive_account ? 'true' : false, 'auto_start_test_drive_onboarding' => $auto_start_test_drive_onboarding ? 'true' : false, + // These are starting capabilities for the account. + // They are collected by the payment method step of the + // WC Payments settings page native onboarding experience. + 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ), 'from' => WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, 'source' => $onboarding_source, 'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false, @@ -1534,13 +1538,19 @@ public function maybe_handle_onboarding() { && WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD !== $from && ! $this->is_stripe_connected() ) { + $additional_params = [ + 'source' => $onboarding_source, + ]; + + if ( $this->onboarding_service->get_capabilities_from_request() ) { + $additional_params['capabilities'] = rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ); + } + $this->redirect_service->redirect_to_onboarding_wizard( // When we redirect to the onboarding wizard, we carry over the `from`, if we have it. // This is because there is no interim step between the user clicking the connect link and the onboarding wizard. ! empty( $from ) ? $from : $next_step_from, - [ - 'source' => $onboarding_source, - ] + $additional_params ); return; } @@ -1573,11 +1583,15 @@ public function maybe_handle_onboarding() { null, $from, // Carry over `from` since we are doing a short-circuit. [ - 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, - 'test_drive' => 'true', + 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, + 'test_drive' => 'true', 'auto_start_test_drive_onboarding' => 'true', // This is critical. - 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, - 'source' => $onboarding_source, + // These are starting capabilities for the account. + // They are collected by the payment method step of the + // WC Payments settings page native onboarding experience. + 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ), + 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, + 'source' => $onboarding_source, 'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false, ] ); @@ -1982,6 +1996,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne } $self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : []; + if ( 'test_drive' === $setup_mode ) { // If we get to the overview page, we want to show the success message. $return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url ); @@ -1996,7 +2011,14 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne ]; $user_data = $this->onboarding_service->get_onboarding_user_data(); - $account_data = $this->onboarding_service->get_account_data( $setup_mode, $self_assessment_data ); + $account_data = $this->onboarding_service->get_account_data( + $setup_mode, + $self_assessment_data, + // These are starting capabilities for the account. + // They are collected by the payment method step of the + // WC Payments settings page native onboarding experience. + $this->onboarding_service->get_capabilities_from_request() + ); $onboarding_data = $this->payments_api_client->get_onboarding_data( 'live' === $setup_mode, @@ -2594,7 +2616,9 @@ public function get_lifetime_total_payment_volume(): int { } /** - * Extract the test drive settings from the account data that we want to store for the live account. + * Extract the useful test drive settings from the account data. + * + * We will use this data to migrate the test drive settings when onboarding the live account. * ATM we only store the enabled payment methods. * * @return array The test drive settings for the live account. diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index ce558ac2faf..504ac3bd5e4 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -180,6 +180,69 @@ function () use ( $country_code, $locale ) { ); } + /** + * Get the onboarding capabilities from the request. + * + * The capabilities are expected to be passed as an array of capabilities keyed by the capability ID and + * with boolean values. If the value is true, the capability is requested when the account is created. + * + * @return array The standardized capabilities that were passed in the request. + * Empty array if no capabilities were passed or none were valid. + */ + public function get_capabilities_from_request(): array { + $capabilities = []; + + if ( empty( $_REQUEST['capabilities'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended + return $capabilities; + } + + // Try to extract the capabilities. + // They might be already decoded or not, so we need to handle both cases. + // We expect them to be an array. + // We disable the warning because we have our own sanitization and validation. + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $capabilities = wp_unslash( $_REQUEST['capabilities'] ); + if ( ! is_array( $capabilities ) ) { + $capabilities = json_decode( $capabilities, true ) ?? []; + } + + if ( empty( $capabilities ) ) { + return []; + } + + // Sanitize and validate. + $capabilities = array_combine( + array_map( + function ( $key ) { + // Keep numeric keys as integers so we can remove them later. + if ( is_numeric( $key ) ) { + return intval( $key ); + } + + return sanitize_text_field( $key ); + }, + array_keys( $capabilities ) + ), + array_map( + function ( $value ) { + return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); + }, + $capabilities + ) + ); + + // Filter out any invalid entries. + $capabilities = array_filter( + $capabilities, + function ( $value, $key ) { + return is_string( $key ) && is_bool( $value ); + }, + ARRAY_FILTER_USE_BOTH + ); + + return $capabilities; + } + /** * Retrieve the embedded KYC session and handle initial account creation (if necessary). * @@ -207,15 +270,19 @@ public function create_embedded_kyc_session( array $self_assessment_data, bool $ 'site_locale' => get_locale(), ]; $user_data = $this->get_onboarding_user_data(); - $account_data = $this->get_account_data( $setup_mode, $self_assessment_data ); + $account_data = $this->get_account_data( + $setup_mode, + $self_assessment_data, + $this->get_capabilities_from_request() + ); $actioned_notes = self::get_actioned_notes(); try { $account_session = $this->payments_api_client->initialize_onboarding_embedded_kyc( 'live' === $setup_mode, $site_data, - array_filter( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. - array_filter( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. + WC_Payments_Utils::array_filter_recursive( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. + WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. $actioned_notes, $progressive ); @@ -365,12 +432,15 @@ public function add_admin_body_classes( string $classes = '' ): string { /** * Get account data for onboarding from self assessment data. * - * @param string $setup_mode Setup mode. + * @param string $setup_mode Setup mode. * @param array $self_assessment_data Self assessment data. + * @param array $capabilities Optional. List keyed by capabilities IDs (payment methods) with boolean values. + * If the value is true, the capability is requested when the account is created. + * If the value is false, the capability is not requested when the account is created. * * @return array Account data. */ - public function get_account_data( string $setup_mode, array $self_assessment_data ): array { + public function get_account_data( string $setup_mode, array $self_assessment_data, array $capabilities = [] ): array { $home_url = get_home_url(); // If the site is running on localhost, use a bogus URL. This is to avoid Stripe's errors. // wp_http_validate_url does not check that, unfortunately. @@ -387,6 +457,33 @@ public function get_account_data( string $setup_mode, array $self_assessment_dat 'business_name' => get_bloginfo( 'name' ), ]; + foreach ( $capabilities as $capability => $should_request ) { + // Remove the `_payments` suffix from the capability, if present. + if ( strpos( $capability, '_payments' ) === strlen( $capability ) - 9 ) { + $capability = str_replace( '_payments', '', $capability ); + } + + // Skip the special 'apple_google' because it is not a payment method. + // Skip the 'woopay' because it is automatically handled by the API. + if ( 'apple_google' === $capability || 'woopay' === $capability ) { + continue; + } + + if ( 'card' === $capability ) { + // Card is always requested. + $account_data['capabilities']['card_payments'] = [ 'requested' => 'true' ]; + // When requesting card, we also need to request transfers. + // The platform should handle this automatically, but it is best to be thorough. + $account_data['capabilities']['transfers'] = [ 'requested' => 'true' ]; + continue; + } + + // We only request, not unrequest capabilities. + if ( $should_request ) { + $account_data['capabilities'][ $capability . '_payments' ] = [ 'requested' => 'true' ]; + } + } + if ( ! empty( $self_assessment_data ) ) { $business_type = $self_assessment_data['business_type'] ?? null; $account_data = WC_Payments_Utils::array_merge_recursive_distinct( @@ -436,6 +533,7 @@ public function get_account_data( string $setup_mode, array $self_assessment_dat ] ); } + return $account_data; }