diff --git a/.husky/post-merge b/.husky/post-merge index 2d66c62ad2b..e7d267f5217 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -22,9 +22,9 @@ else echo "\033[32mDetermining if there is an update for the WCPay Dev Tools plugin...\033[0m" DEV_TOOLS_BRANCH=$(cd $DEV_TOOLS_PLUGIN_PATH && git branch --show-current) - if [[ $DEV_TOOLS_BRANCH = "trunk" ]]; then + if [ $DEV_TOOLS_BRANCH = "trunk" ]; then echo " \033[32mThe current branch is trunk. Check if we are safe to pull from origin/trunk...\033[0m" - if [[ `cd $DEV_TOOLS_PLUGIN_PATH && git status --porcelain` ]]; then + if [ `cd $DEV_TOOLS_PLUGIN_PATH && git status --porcelain` ]; then echo "\033[33m There are uncommitted local changes on the WCPay Dev Tools repo. Skipping any attempt to update it.\033[0m" else echo " \033[32mPulling the latest changes from origin/trunk, if any...\033[0m" diff --git a/assets/css/admin.css b/assets/css/admin.css index 38a9de18a9d..5977ca3ac17 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -131,6 +131,14 @@ background-image: url( '../images/payment-methods/afterpay-icon.svg' ); } +.payment-method__brand--afterpay_clearpay.account-country--gb { + background-image: url( '../images/payment-methods/clearpay.svg' ); +} + +.payment-method__brand--afterpay_clearpay.account-country--us { + background-image: url( '../images/payment-methods/afterpay-cashapp-icon.svg' ); +} + .payment-method__brand--affirm { background-image: url( '../images/payment-methods/affirm-icon.svg' ); } diff --git a/assets/css/success.rtl.css b/assets/css/success.rtl.css index 42e87b13938..75721e05337 100644 --- a/assets/css/success.rtl.css +++ b/assets/css/success.rtl.css @@ -148,7 +148,7 @@ color: #4d3716; justify-self: start; width: max-content; - margin-left: 5px; + margin-right: 5px; } #wc-payment-gateway-multibanco-instructions-container .payment-box-value { diff --git a/assets/images/payment-methods/afterpay-cashapp-badge.svg b/assets/images/payment-methods/afterpay-cashapp-badge.svg new file mode 100644 index 00000000000..8dd11afd005 --- /dev/null +++ b/assets/images/payment-methods/afterpay-cashapp-badge.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/payment-methods/afterpay-cashapp-icon.svg b/assets/images/payment-methods/afterpay-cashapp-icon.svg new file mode 100644 index 00000000000..bc3efde8789 --- /dev/null +++ b/assets/images/payment-methods/afterpay-cashapp-icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/payment-methods/afterpay-cashapp-logo-dark.svg b/assets/images/payment-methods/afterpay-cashapp-logo-dark.svg new file mode 100644 index 00000000000..0184809985a --- /dev/null +++ b/assets/images/payment-methods/afterpay-cashapp-logo-dark.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/payment-methods/afterpay-cashapp-logo.svg b/assets/images/payment-methods/afterpay-cashapp-logo.svg new file mode 100644 index 00000000000..fe36629c7f6 --- /dev/null +++ b/assets/images/payment-methods/afterpay-cashapp-logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index 672d1d9d3ef..3a7f9291530 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,43 @@ *** WooPayments Changelog *** += 9.2.0 - 2025-04-09 = +* Add - Add back button for tertiary+ level pages in WooPayments settings. +* Fix - fix: cancel GooglePay/ApplePay dialog on product page if add-to-cart product validation fails +* Fix - fix: fatal error when Klarna is enabled on an EU account and a non-EU currency is configured on the store. +* Fix - fix: Google Pay/Apple Pay display on pay-for-order pages. +* Fix - Fix deprecated hook woocommerce_rest_api_option_permissions +* Fix - Fix errors in WooCommerce email settings preview +* Fix - Fix Multi-currency conversion for WooCommerce Bookings range type cost adjustments +* Fix - Fix PMME display on shortcode cart with block-based themes. +* Fix - Fix WooPay enabled during NOX onboarding despite being disabled in recommended payment methods. +* Fix - Handle pending refunds properly +* Fix - Linked account ID to product ID to maintain consistency and prevent issues when the account ID changes. +* Fix - Prevent unsaved changes dialog when changes have been saved. +* Fix - Removed hard-coded lists of payment methods where possible. +* Fix - Remove unused wcpay_date_format_notice_dismissed option from the permission list +* Fix - Set background color to white for the Payments settings page +* Fix - update: ensure Google Pay/Apple Pay honor 'Display prices during cart and checkout' setting +* Fix - Update WooPay icon on order page. +* Update - Added _wcpay_net to the metadata. +* Update - Chore: check the array type in dismissed noticeces component. +* Update - chore: disable request of JCB capability +* Update - fix: Google Pay/Apple Pay HK test address override. +* Update - fix: parsing of error message for GooglePay/ApplePay buttons to be displayed to customer, instead of displaying generic error message on failure. +* Update - Improve the ECE container loading experience. +* Update - Move payment method map definition to the backend +* Update - Prevent creation of the renewal orders if original order was created in the different WooPayments mode. +* Update - refactor: delete temporary Google Pay/Apple Pay cart contents right after making the request, to improve performance and avoiding bots sending wrong session data in subsequent requests. +* Update - Remove fraud protection discoverability and update tour +* Update - Stripe Billing and Manual Capture incompatibility notice on the Settings page. +* Update - Update Settings page as per the new design +* Update - Update to Cash App Afterpay branding. +* Dev - Bump WC tested up to version to 9.7.1. +* Dev - Fix unneeded double square brackets in the post-merge script +* Dev - Removed the deprecated wcpay_exit_survey_dismissed option from the ALLOWED_OPTIONS list. +* Dev - Remove level3 retry logic and legacy request_with_level3_data method +* Dev - Updated the progressive parameter in the KYC session creation API to use a boolean type. +* Dev - We switch to using site instead of url as the key in the self assessment data to avoid XSS firewall false-positives. + = 9.1.0 - 2025-03-19 = * Add - Add a prompt to encourage merchants to provide feedback or leave a WordPress.org review for WooPayments * Add - Add order failure message on Order Received page. diff --git a/client/additional-methods-setup/constants.js b/client/additional-methods-setup/constants.js index f33be757bbd..886633e4d53 100644 --- a/client/additional-methods-setup/constants.js +++ b/client/additional-methods-setup/constants.js @@ -1,22 +1,3 @@ -export const upeMethods = [ - 'au_becs_debit', - 'alipay', - 'bancontact', - 'eps', - 'giropay', - 'ideal', - 'p24', - 'sepa_debit', - 'sofort', - 'affirm', - 'afterpay_clearpay', - 'jcb', - 'klarna', - 'multibanco', - 'grabpay', - 'wechat_pay', -]; - export const upeCapabilityStatuses = { PENDING_VERIFICATION: 'pending_verification', PENDING_APPROVAL: 'pending', diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 3f9a949d46d..542ab4c6581 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -27,24 +27,6 @@ import { tokenizedExpressCheckoutElementGooglePay, } from 'wcpay/tokenized-express-checkout/blocks'; -import { - PAYMENT_METHOD_NAME_CARD, - PAYMENT_METHOD_NAME_ALIPAY, - PAYMENT_METHOD_NAME_BANCONTACT, - PAYMENT_METHOD_NAME_BECS, - PAYMENT_METHOD_NAME_EPS, - PAYMENT_METHOD_NAME_GIROPAY, - PAYMENT_METHOD_NAME_IDEAL, - PAYMENT_METHOD_NAME_P24, - PAYMENT_METHOD_NAME_SEPA, - PAYMENT_METHOD_NAME_SOFORT, - PAYMENT_METHOD_NAME_AFFIRM, - PAYMENT_METHOD_NAME_AFTERPAY, - PAYMENT_METHOD_NAME_KLARNA, - PAYMENT_METHOD_NAME_MULTIBANCO, - PAYMENT_METHOD_NAME_GRABPAY, - PAYMENT_METHOD_NAME_WECHAT_PAY, -} from '../constants.js'; import { getDeferredIntentCreationUPEFields } from './payment-elements'; import { handleWooPayEmailInput } from '../woopay/email-input-iframe'; import { recordUserEvent } from 'tracks'; @@ -52,25 +34,6 @@ import wooPayExpressCheckoutPaymentMethod from '../woopay/express-button/woopay- import { isPreviewing } from '../preview'; import '../utils/copy-test-number'; -const upeMethods = { - card: PAYMENT_METHOD_NAME_CARD, - alipay: PAYMENT_METHOD_NAME_ALIPAY, - bancontact: PAYMENT_METHOD_NAME_BANCONTACT, - au_becs_debit: PAYMENT_METHOD_NAME_BECS, - eps: PAYMENT_METHOD_NAME_EPS, - giropay: PAYMENT_METHOD_NAME_GIROPAY, - ideal: PAYMENT_METHOD_NAME_IDEAL, - p24: PAYMENT_METHOD_NAME_P24, - sepa_debit: PAYMENT_METHOD_NAME_SEPA, - sofort: PAYMENT_METHOD_NAME_SOFORT, - affirm: PAYMENT_METHOD_NAME_AFFIRM, - afterpay_clearpay: PAYMENT_METHOD_NAME_AFTERPAY, - klarna: PAYMENT_METHOD_NAME_KLARNA, - multibanco: PAYMENT_METHOD_NAME_MULTIBANCO, - grabpay: PAYMENT_METHOD_NAME_GRABPAY, - wechat_pay: PAYMENT_METHOD_NAME_WECHAT_PAY, -}; - const enabledPaymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); const isStripeLinkEnabled = isLinkEnabled( enabledPaymentMethodsConfig ); @@ -90,16 +53,16 @@ Object.entries( enabledPaymentMethodsConfig ) .filter( ( [ upeName ] ) => upeName !== 'link' ) .forEach( ( [ upeName, upeConfig ] ) => { registerPaymentMethod( { - name: upeMethods[ upeName ], + name: upeConfig.gatewayId, content: getDeferredIntentCreationUPEFields( upeName, - upeMethods, + enabledPaymentMethodsConfig, api, upeConfig.testingInstructions ), edit: getDeferredIntentCreationUPEFields( upeName, - upeMethods, + enabledPaymentMethodsConfig, api, upeConfig.testingInstructions ), @@ -114,7 +77,7 @@ Object.entries( enabledPaymentMethodsConfig ) // We used to check if stripe was loaded with `getStripeForUPE`, but we can't guarantee it will be loaded synchronously. return needsPayment && isAvailableInTheCountry; }, - paymentMethodId: upeMethods[ upeName ], + paymentMethodId: upeConfig.gatewayId, // see .wc-block-checkout__payment-method styles in blocks/style.scss label: ( paymentMethodsConfig[ key ].isBnpl +); + const PaymentMethodMessageWrapper = ( { upeName, countries, diff --git a/client/checkout/blocks/payment-processor.js b/client/checkout/blocks/payment-processor.js index 44c9613691c..5b9c94e3a32 100644 --- a/client/checkout/blocks/payment-processor.js +++ b/client/checkout/blocks/payment-processor.js @@ -19,15 +19,13 @@ import { blocksShowLinkButtonHandler, getBlocksEmailValue, isLinkEnabled, + getGatewayIdBy, } from 'wcpay/checkout/utils/upe'; import { useCustomerData } from './utils'; import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link'; import { getUPEConfig } from 'wcpay/utils/checkout'; import { validateElements } from 'wcpay/checkout/classic/payment-processing'; -import { - PAYMENT_METHOD_ERROR, - PAYMENT_METHOD_NAME_CARD, -} from 'wcpay/checkout/constants'; +import { PAYMENT_METHOD_ERROR } from 'wcpay/checkout/constants'; const getBillingDetails = ( billingData ) => { return { @@ -72,7 +70,8 @@ const PaymentProcessor = ( { const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); const isTestMode = getUPEConfig( 'testMode' ); - const gatewayConfig = getPaymentMethods()[ upeMethods[ paymentMethodId ] ]; + const gatewayId = upeMethods[ paymentMethodId ].gatewayId; + const gatewayConfig = getPaymentMethods()[ gatewayId ]; const { billingAddress: billingData, setShippingAddress, @@ -81,7 +80,7 @@ const PaymentProcessor = ( { useEffect( () => { if ( - activePaymentMethod === PAYMENT_METHOD_NAME_CARD && + activePaymentMethod === getGatewayIdBy( 'card' ) && isLinkEnabled( paymentMethodsConfig ) ) { enableStripeLinkPaymentMethod( { @@ -150,9 +149,7 @@ const PaymentProcessor = ( { () => onPaymentSetup( () => { async function handlePaymentProcessing() { - if ( - upeMethods[ paymentMethodId ] !== activePaymentMethod - ) { + if ( gatewayId !== activePaymentMethod ) { return; } @@ -214,8 +211,7 @@ const PaymentProcessor = ( { type: 'success', meta: { paymentMethodData: { - payment_method: - upeMethods[ paymentMethodId ], + payment_method: gatewayId, 'wcpay-payment-method': PAYMENT_METHOD_ERROR, 'wcpay-payment-method-error-code': result.error.code, @@ -236,7 +232,7 @@ const PaymentProcessor = ( { type: 'success', meta: { paymentMethodData: { - payment_method: upeMethods[ paymentMethodId ], + payment_method: gatewayId, 'wcpay-payment-method': result.paymentMethod.id, 'wcpay-fraud-prevention-token': getFraudPreventionToken(), 'wcpay-fingerprint': fingerprint, @@ -255,7 +251,7 @@ const PaymentProcessor = ( { paymentMethodId, paymentMethodsConfig, shouldSavePayment, - upeMethods, + gatewayId, errorMessage, onPaymentSetup, billingData, diff --git a/client/checkout/blocks/test/payment-processor.test.js b/client/checkout/blocks/test/payment-processor.test.js index 5926fea88fc..17439a3ff8b 100644 --- a/client/checkout/blocks/test/payment-processor.test.js +++ b/client/checkout/blocks/test/payment-processor.test.js @@ -113,7 +113,7 @@ describe( 'PaymentProcessor', () => { } } fingerprint="" shouldSavePayment={ false } - upeMethods={ { card: 'woocommerce_payments' } } + upeMethods={ { card: { gatewayId: 'woocommerce_payments' } } } onLoadError={ jest.fn() } /> ); @@ -143,7 +143,9 @@ describe( 'PaymentProcessor', () => { } } fingerprint="" shouldSavePayment={ false } - upeMethods={ { card: 'woocommerce_payments' } } + upeMethods={ { + card: { gatewayId: 'woocommerce_payments' }, + } } /> ); } ); @@ -179,7 +181,9 @@ describe( 'PaymentProcessor', () => { } } fingerprint="" shouldSavePayment={ false } - upeMethods={ { card: 'woocommerce_payments' } } + upeMethods={ { + card: { gatewayId: 'woocommerce_payments' }, + } } /> ); } ); @@ -225,7 +229,9 @@ describe( 'PaymentProcessor', () => { } } fingerprint="" shouldSavePayment={ false } - upeMethods={ { card: 'woocommerce_payments' } } + upeMethods={ { + card: { gatewayId: 'woocommerce_payments' }, + } } /> ); } ); diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index 7eb87d79481..cf73bebfc2e 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -163,7 +163,12 @@ jQuery( function ( $ ) { } async function injectStripePMMEContainers() { - const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; + const paymentMethodsConfig = + getUPEConfig( 'paymentMethodsConfig' ) || {}; + const bnplMethods = Object.keys( paymentMethodsConfig ).filter( + ( key ) => paymentMethodsConfig[ key ]?.isBnpl + ); + const labelBase = 'payment_method_woocommerce_payments_'; const paymentMethods = getUPEConfig( 'paymentMethodsConfig' ); const paymentMethodsKeys = Object.keys( paymentMethods ); diff --git a/client/checkout/classic/payment-processing.js b/client/checkout/classic/payment-processing.js index cfde44fcb5d..64f8982c76f 100644 --- a/client/checkout/classic/payment-processing.js +++ b/client/checkout/classic/payment-processing.js @@ -124,6 +124,9 @@ function submitForm( jQueryForm ) { /** * Validates the contents of the address fields based on the requirements from BNPL payment methods. * + * FLAG: PAYMENT_METHODS_LIST + * This is specifically looking for Afterpay and Affirm payment methods - not all BNPL methods. + * * @param {Object} params The parameters to be sent to `createPaymentMethod`. * @param {string} paymentMethodType The type of Stripe payment method to create. * @return {boolean} True, if there are missing address fields. False, if the validation passes or is not applicable. diff --git a/client/checkout/constants.js b/client/checkout/constants.js index 588e74c5885..c0627a95f10 100644 --- a/client/checkout/constants.js +++ b/client/checkout/constants.js @@ -1,20 +1,3 @@ -export const PAYMENT_METHOD_NAME_CARD = 'woocommerce_payments'; -export const PAYMENT_METHOD_NAME_ALIPAY = 'woocommerce_payments_alipay'; -export const PAYMENT_METHOD_NAME_BANCONTACT = 'woocommerce_payments_bancontact'; -export const PAYMENT_METHOD_NAME_BECS = 'woocommerce_payments_au_becs_debit'; -export const PAYMENT_METHOD_NAME_EPS = 'woocommerce_payments_eps'; -export const PAYMENT_METHOD_NAME_GIROPAY = 'woocommerce_payments_giropay'; -export const PAYMENT_METHOD_NAME_IDEAL = 'woocommerce_payments_ideal'; -export const PAYMENT_METHOD_NAME_P24 = 'woocommerce_payments_p24'; -export const PAYMENT_METHOD_NAME_SEPA = 'woocommerce_payments_sepa_debit'; -export const PAYMENT_METHOD_NAME_SOFORT = 'woocommerce_payments_sofort'; -export const PAYMENT_METHOD_NAME_AFFIRM = 'woocommerce_payments_affirm'; -export const PAYMENT_METHOD_NAME_AFTERPAY = - 'woocommerce_payments_afterpay_clearpay'; -export const PAYMENT_METHOD_NAME_KLARNA = 'woocommerce_payments_klarna'; -export const PAYMENT_METHOD_NAME_MULTIBANCO = 'woocommerce_payments_multibanco'; -export const PAYMENT_METHOD_NAME_GRABPAY = 'woocommerce_payments_grabpay'; -export const PAYMENT_METHOD_NAME_WECHAT_PAY = 'woocommerce_payments_wechat_pay'; export const PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT = 'woocommerce_payments_express_checkout'; export const PAYMENT_METHOD_NAME_WOOPAY_EXPRESS_CHECKOUT = @@ -22,27 +5,6 @@ export const PAYMENT_METHOD_NAME_WOOPAY_EXPRESS_CHECKOUT = export const PAYMENT_METHOD_ERROR = 'woocommerce_payments_payment_method_error'; export const WC_STORE_CART = 'wc/store/cart'; -export function getPaymentMethodsConstants() { - return [ - PAYMENT_METHOD_NAME_ALIPAY, - PAYMENT_METHOD_NAME_BANCONTACT, - PAYMENT_METHOD_NAME_BECS, - PAYMENT_METHOD_NAME_EPS, - PAYMENT_METHOD_NAME_GIROPAY, - PAYMENT_METHOD_NAME_IDEAL, - PAYMENT_METHOD_NAME_P24, - PAYMENT_METHOD_NAME_SEPA, - PAYMENT_METHOD_NAME_SOFORT, - PAYMENT_METHOD_NAME_AFFIRM, - PAYMENT_METHOD_NAME_AFTERPAY, - PAYMENT_METHOD_NAME_CARD, - PAYMENT_METHOD_NAME_KLARNA, - PAYMENT_METHOD_NAME_MULTIBANCO, - PAYMENT_METHOD_NAME_GRABPAY, - PAYMENT_METHOD_NAME_WECHAT_PAY, - ]; -} - export const SHORTCODE_SHIPPING_ADDRESS_FIELDS = { address_1: 'shipping_address_1', address_2: 'shipping_address_2', diff --git a/client/checkout/express-checkout-buttons.scss b/client/checkout/express-checkout-buttons.scss index c153906b5a5..b99d65f23b7 100644 --- a/client/checkout/express-checkout-buttons.scss +++ b/client/checkout/express-checkout-buttons.scss @@ -1,22 +1,36 @@ .wcpay-express-checkout-wrapper { - margin-top: 1em; width: 100%; clear: both; + display: flex; + flex-direction: column; + + #wcpay-express-checkout-element { + opacity: 0; + transition: all 0.3s ease-in-out; + min-height: 0; + margin: 0; + + &.is-ready { + opacity: 1; + + &:not( :first-child ) { + margin-top: 12px; + } + } + } + .woocommerce-cart & { margin-bottom: 0; } .woocommerce-checkout & { - margin-top: 0; - } - - > div { - margin: 4px; - margin-bottom: 12px; + #wcpay-express-checkout-element.is-ready:first-child { + margin-top: 4px; + } - &:last-of-type { - margin-bottom: 0; + #wcpay-woopay-button { + margin-top: 4px; } } } diff --git a/client/checkout/utils/test/upe.test.js b/client/checkout/utils/test/upe.test.js index eeb4a62f119..29e6566eb84 100644 --- a/client/checkout/utils/test/upe.test.js +++ b/client/checkout/utils/test/upe.test.js @@ -15,8 +15,6 @@ import { isBillingInformationMissing, } from '../upe'; -import { getPaymentMethodsConstants } from '../../constants'; - import { getUPEConfig } from 'wcpay/utils/checkout'; jest.mock( 'wcpay/utils/checkout' ); @@ -563,23 +561,34 @@ describe( 'UPE checkout utils', () => { describe( 'generateCheckoutEventNames', () => { it( 'should return empty string when there are no payment methods', () => { - getPaymentMethodsConstants.mockImplementation( () => [] ); - + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'paymentMethodsConfig' ) { + return {}; + } + } ); const result = generateCheckoutEventNames(); expect( result ).toEqual( '' ); } ); it( 'should generate correct event names when there are payment methods', () => { - getPaymentMethodsConstants.mockImplementation( () => [ - 'woocommerce_payments_bancontact', - 'woocommerce_payments_eps', - ] ); + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'paymentMethodsConfig' ) { + return { + test_method_one: { + gatewayId: 'woocommerce_payments_test_method_one', + }, + test_method_two: { + gatewayId: 'woocommerce_payments_test_method_two', + }, + }; + } + } ); const result = generateCheckoutEventNames(); expect( result ).toEqual( - 'checkout_place_order_woocommerce_payments_bancontact checkout_place_order_woocommerce_payments_eps' + 'checkout_place_order_woocommerce_payments_test_method_one checkout_place_order_woocommerce_payments_test_method_two' ); } ); } ); diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js index 61bcfb71334..2f7e4c3117e 100644 --- a/client/checkout/utils/upe.js +++ b/client/checkout/utils/upe.js @@ -4,10 +4,7 @@ * Internal dependencies */ import { getUPEConfig } from 'wcpay/utils/checkout'; -import { - getPaymentMethodsConstants, - SHORTCODE_BILLING_ADDRESS_FIELDS, -} from '../constants'; +import { SHORTCODE_BILLING_ADDRESS_FIELDS } from '../constants'; /** * Generates terms for reusable payment methods @@ -130,13 +127,13 @@ export const getUpeSettings = ( paymentMethodType ) => { return upeSettings; }; -function getGatewayIdBy( paymentMethodType ) { +export const getGatewayIdBy = ( paymentMethodType ) => { const gatewayPrefix = 'woocommerce_payments'; // Only append underscore and payment method type for non-card payments return paymentMethodType === 'card' ? gatewayPrefix : `${ gatewayPrefix }_${ paymentMethodType }`; -} +}; function shouldIncludeTerms( paymentMethodType ) { if ( getUPEConfig( 'cartContainsSubscription' ) ) { @@ -158,8 +155,8 @@ function shouldIncludeTerms( paymentMethodType ) { } export const generateCheckoutEventNames = () => { - return getPaymentMethodsConstants() - .map( ( method ) => `checkout_place_order_${ method }` ) + return Object.values( getUPEConfig( 'paymentMethodsConfig' ) ) + .map( ( method ) => `checkout_place_order_${ method.gatewayId }` ) .join( ' ' ); }; diff --git a/client/components/account-balances/index.tsx b/client/components/account-balances/index.tsx index d2ddfb8b825..b7b97cfe869 100644 --- a/client/components/account-balances/index.tsx +++ b/client/components/account-balances/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import React, { useState } from 'react'; -import { useDispatch } from '@wordpress/data'; import { Card, CardBody, CardHeader, Flex } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import interpolateComponents from '@automattic/interpolate-components'; @@ -26,10 +25,10 @@ import { ClickTooltip } from '../tooltip'; import { formatCurrency } from 'multi-currency/interface/functions'; import { useAllDepositsOverviews } from 'wcpay/data'; import { useSelectedCurrency } from 'wcpay/overview/hooks'; +import { saveOption } from 'wcpay/data/settings/actions'; import './style.scss'; const useInstantDepositNoticeState = () => { - const { updateOptions } = useDispatch( 'wc/admin/options' ); const [ isDismissed, setIsDismissed ] = useState( wcpaySettings.isInstantDepositNoticeDismissed ); @@ -37,7 +36,7 @@ const useInstantDepositNoticeState = () => { const setInstantDepositNoticeDismissed = () => { setIsDismissed( true ); wcpaySettings.isInstantDepositNoticeDismissed = true; - updateOptions( { wcpay_instant_deposit_notice_dismissed: true } ); + saveOption( 'wcpay_instant_deposit_notice_dismissed', true ); }; return { diff --git a/client/components/account-status/account-fees/test/index.js b/client/components/account-status/account-fees/test/index.js index 7405b33e371..6dafb6fc16f 100644 --- a/client/components/account-status/account-fees/test/index.js +++ b/client/components/account-status/account-fees/test/index.js @@ -48,6 +48,15 @@ describe( 'AccountFees', () => { }, dateFormat: 'F j, Y', }; + + global.wooPaymentsPaymentMethodsConfig = { + giropay: { + title: 'giropay', + }, + sofort: { + title: 'Sofort', + }, + }; } ); test( 'renders normal base fee', () => { diff --git a/client/components/duplicate-notice/index.tsx b/client/components/duplicate-notice/index.tsx index faca5adb573..12a008f3aaa 100644 --- a/client/components/duplicate-notice/index.tsx +++ b/client/components/duplicate-notice/index.tsx @@ -5,8 +5,12 @@ import React, { useCallback } from 'react'; import InlineNotice from '../inline-notice'; import interpolateComponents from '@automattic/interpolate-components'; import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ import { getAdminUrl } from 'wcpay/utils'; -import { useDispatch } from '@wordpress/data'; +import { saveOption } from 'wcpay/data/settings/actions'; export type PaymentMethodToPluginsMap = { [ key: string ]: string[] }; interface DuplicateNoticeProps { @@ -24,11 +28,9 @@ function DuplicateNotice( { dismissedNotices, setDismissedDuplicateNotices, }: DuplicateNoticeProps ): JSX.Element | null { - const { updateOptions } = useDispatch( 'wc/admin/options' ); - const handleDismiss = useCallback( () => { const updatedNotices = { ...dismissedNotices }; - if ( updatedNotices[ paymentMethod ] ) { + if ( Array.isArray( updatedNotices[ paymentMethod ] ) ) { // If there are existing dismissed notices for the payment method, append to the current array. updatedNotices[ paymentMethod ] = [ ...new Set( [ @@ -41,19 +43,19 @@ function DuplicateNotice( { } setDismissedDuplicateNotices( updatedNotices ); - updateOptions( { - wcpay_duplicate_payment_method_notices_dismissed: updatedNotices, - } ); + saveOption( + 'wcpay_duplicate_payment_method_notices_dismissed', + updatedNotices + ); wcpaySettings.dismissedDuplicateNotices = updatedNotices; }, [ paymentMethod, gatewaysEnablingPaymentMethod, dismissedNotices, setDismissedDuplicateNotices, - updateOptions, ] ); - if ( dismissedNotices?.[ paymentMethod ] ) { + if ( Array.isArray( dismissedNotices?.[ paymentMethod ] ) ) { const isNoticeDismissedForEveryGateway = gatewaysEnablingPaymentMethod.every( ( value ) => dismissedNotices[ paymentMethod ].includes( value ) ); diff --git a/client/components/duplicate-notice/tests/index.test.tsx b/client/components/duplicate-notice/tests/index.test.tsx index 290f756d650..3f2a660c905 100644 --- a/client/components/duplicate-notice/tests/index.test.tsx +++ b/client/components/duplicate-notice/tests/index.test.tsx @@ -3,25 +3,20 @@ */ import React from 'react'; import { render, fireEvent, screen, cleanup } from '@testing-library/react'; -import { useDispatch } from '@wordpress/data'; /** * Internal dependencies */ import DuplicateNotice from '..'; +import { saveOption } from 'wcpay/data/settings/actions'; -jest.mock( '@wordpress/data', () => ( { - useDispatch: jest.fn(), +jest.mock( 'wcpay/data/settings/actions', () => ( { + saveOption: jest.fn(), } ) ); -const mockUseDispatch = useDispatch as jest.MockedFunction< any >; +const mockSaveOption = saveOption as jest.MockedFunction< any >; describe( 'DuplicateNotice', () => { - const mockDispatch = jest.fn(); - mockUseDispatch.mockReturnValue( { - updateOptions: mockDispatch, - } ); - afterEach( () => { cleanup(); } ); @@ -108,11 +103,12 @@ describe( 'DuplicateNotice', () => { expect( props.setDismissedDuplicateNotices ).toHaveBeenCalledWith( { [ paymentMethod ]: [ 'woocommerce_payments' ], } ); - expect( mockDispatch ).toHaveBeenCalledWith( { - wcpay_duplicate_payment_method_notices_dismissed: { - [ paymentMethod ]: [ 'woocommerce_payments' ], - }, - } ); + expect( + mockSaveOption + ).toHaveBeenCalledWith( + 'wcpay_duplicate_payment_method_notices_dismissed', + { [ paymentMethod ]: [ 'woocommerce_payments' ] } + ); } ); test( 'clicking on the Review extensions link navigates correctly', () => { diff --git a/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx b/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx deleted file mode 100644 index 8c96220a23e..00000000000 --- a/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { recordEvent } from 'tracks'; - -interface BannerActionsProps { - handleDontShowAgainOnClick: () => void; -} - -const BannerActions: React.FC< BannerActionsProps > = ( { - handleDontShowAgainOnClick, -} ) => { - const handleLearnMoreButtonClick = () => { - recordEvent( - 'wcpay_fraud_protection_banner_learn_more_button_clicked' - ); - }; - - return ( -
- - -
- ); -}; - -export default BannerActions; diff --git a/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 7f2fd80d510..00000000000 --- a/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BannerActions renders 1`] = ` -
-
- - Learn more - - -
-
-`; diff --git a/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx b/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx deleted file mode 100644 index 7fa3df47b61..00000000000 --- a/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import BannerActions from '..'; - -const mockHandleDontShowAgainOnClick = jest.fn(); - -describe( 'BannerActions', () => { - it( 'renders', () => { - const { container: bannerActionsComponent } = render( - - ); - - expect( bannerActionsComponent ).toMatchSnapshot(); - } ); -} ); diff --git a/client/components/fraud-risk-tools-banner/components/banner-body/index.tsx b/client/components/fraud-risk-tools-banner/components/banner-body/index.tsx deleted file mode 100644 index afcf238aa02..00000000000 --- a/client/components/fraud-risk-tools-banner/components/banner-body/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { __ } from '@wordpress/i18n'; - -const BannerBody: React.FC = () => { - return ( -

- { __( - 'New features have been added to WooPayments to help reduce fraudulent transactions on your store. ' + - 'By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters.', - 'woocommerce-payments' - ) } -

- ); -}; - -export default BannerBody; diff --git a/client/components/fraud-risk-tools-banner/components/banner-body/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/components/banner-body/test/__snapshots__/index.test.tsx.snap deleted file mode 100644 index a0236a5b740..00000000000 --- a/client/components/fraud-risk-tools-banner/components/banner-body/test/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BannerBody renders 1`] = ` -
-

- New features have been added to WooPayments to help reduce fraudulent transactions on your store. By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters. -

-
-`; diff --git a/client/components/fraud-risk-tools-banner/components/banner-body/test/index.test.tsx b/client/components/fraud-risk-tools-banner/components/banner-body/test/index.test.tsx deleted file mode 100644 index 9b5c359f1c0..00000000000 --- a/client/components/fraud-risk-tools-banner/components/banner-body/test/index.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import BannerBody from '..'; - -describe( 'BannerBody', () => { - it( 'renders', () => { - const { container: bannerBodyComponent } = render( ); - - expect( bannerBodyComponent ).toMatchSnapshot(); - } ); -} ); diff --git a/client/components/fraud-risk-tools-banner/components/index.tsx b/client/components/fraud-risk-tools-banner/components/index.tsx deleted file mode 100644 index fd6602558b0..00000000000 --- a/client/components/fraud-risk-tools-banner/components/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Internal dependencies - */ -import NewPill from './new-pill'; -import BannerBody from './banner-body'; -import BannerActions from './banner-actions'; - -export { NewPill, BannerBody, BannerActions }; diff --git a/client/components/fraud-risk-tools-banner/components/new-pill/index.tsx b/client/components/fraud-risk-tools-banner/components/new-pill/index.tsx deleted file mode 100644 index a6882a2dfea..00000000000 --- a/client/components/fraud-risk-tools-banner/components/new-pill/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import Pill from '../../../pill'; - -const NewPill: React.FC = () => { - return ( - - { __( 'New', 'woocommerce-payments' ) } - - ); -}; - -export default NewPill; diff --git a/client/components/fraud-risk-tools-banner/components/new-pill/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/components/new-pill/test/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 3426547886e..00000000000 --- a/client/components/fraud-risk-tools-banner/components/new-pill/test/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NewPill renders 1`] = ` -
- - New - -
-`; diff --git a/client/components/fraud-risk-tools-banner/components/new-pill/test/index.test.tsx b/client/components/fraud-risk-tools-banner/components/new-pill/test/index.test.tsx deleted file mode 100644 index a559ed60226..00000000000 --- a/client/components/fraud-risk-tools-banner/components/new-pill/test/index.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import NewPill from '..'; - -describe( 'NewPill', () => { - it( 'renders', () => { - const { container: newPillComponent } = render( ); - - expect( newPillComponent ).toMatchSnapshot(); - } ); -} ); diff --git a/client/components/fraud-risk-tools-banner/index.tsx b/client/components/fraud-risk-tools-banner/index.tsx deleted file mode 100644 index 04a6c3efc0e..00000000000 --- a/client/components/fraud-risk-tools-banner/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * External dependencies - */ -import React, { useEffect, useState } from 'react'; -import { __ } from '@wordpress/i18n'; -import { Card } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { BannerBody, NewPill, BannerActions } from './components'; -import './style.scss'; -import { recordEvent } from 'tracks'; - -interface BannerSettings { - dontShowAgain: boolean; -} - -const FRTDiscoverabilityBanner: React.FC = () => { - const { frtDiscoverBannerSettings, lifetimeTPV } = wcpaySettings; - const { updateOptions } = useDispatch( 'wc/admin/options' ); - const [ settings, setSettings ] = useState< BannerSettings >( () => { - try { - return JSON.parse( frtDiscoverBannerSettings ); - } catch ( e ) { - return { dontShowAgain: false }; - } - } ); - - const showBanner = lifetimeTPV > 0 && ! settings.dontShowAgain; - - const setDontShowAgain = () => { - setSettings( { dontShowAgain: true } ); - }; - - useEffect( () => { - recordEvent( 'wcpay_fraud_protection_banner_rendered' ); - - const stringifiedSettings = JSON.stringify( settings ); - - updateOptions( { - wcpay_frt_discover_banner_settings: stringifiedSettings, - } ); - - wcpaySettings.frtDiscoverBannerSettings = stringifiedSettings; - }, [ frtDiscoverBannerSettings, settings, updateOptions ] ); - - const handleDontShowAgainOnClick = () => { - setDontShowAgain(); - }; - - if ( ! showBanner ) { - return null; - } - - return ( - -
- -

- { __( - 'Enhanced fraud protection for your store', - 'woocommerce-payments' - ) } -

- - -
-
- ); -}; - -export default FRTDiscoverabilityBanner; diff --git a/client/components/fraud-risk-tools-banner/style.scss b/client/components/fraud-risk-tools-banner/style.scss deleted file mode 100644 index b5d3577946e..00000000000 --- a/client/components/fraud-risk-tools-banner/style.scss +++ /dev/null @@ -1,63 +0,0 @@ -.discoverability-card { - padding: 24px; - background-color: #fff; - - @media ( min-width: 600px ) { - background: #fff - url( '../../../assets/images/fraud-protection/discoverability-banner@2x.png' ) - no-repeat center right; - background-size: 215px 236px; - padding-right: 236px; - } - - &__new-feature-pill.wcpay-pill { - color: #3c087e; - border: 1px solid #3c087e; - font-size: 12px; - line-height: 14px; - border-radius: 14px; - margin: 0; - } - - &__header { - color: #2c045d; - font-style: normal; - font-weight: 400; - font-size: 20px; - line-height: 28px; - margin: 16px 0 8px; - } - - &__body { - color: #2c045d; - font-size: 13px; - line-height: 16px; - margin: 0 0 16px; - } - - &__actions { - display: flex; - flex-direction: column; - - @media ( min-width: 600px ) { - flex-direction: row; - } - - .components-button { - padding-left: 12px; - padding-right: 12px; - - & + .components-button { - margin: 8px 0 0 0; - - @media ( min-width: 600px ) { - margin: 0 0 0 8px; - } - } - - &.is-tertiary { - color: #2c045d; - } - } - } -} diff --git a/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d22e4c5a584..00000000000 --- a/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FRTDiscoverabilityBanner does not render when dontShowAgain is true 1`] = `
`; - -exports[`FRTDiscoverabilityBanner does not render when no transactions are processed 1`] = `
`; - -exports[`FRTDiscoverabilityBanner renders 1`] = ` -
-
-
-
- - New - -

- Enhanced fraud protection for your store -

-

- New features have been added to WooPayments to help reduce fraudulent transactions on your store. By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters. -

-
- - Learn more - - -
-
-
- -`; diff --git a/client/components/fraud-risk-tools-banner/test/index.test.tsx b/client/components/fraud-risk-tools-banner/test/index.test.tsx deleted file mode 100644 index dc5c98b7c30..00000000000 --- a/client/components/fraud-risk-tools-banner/test/index.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * External dependencies - */ -import React, { HTMLAttributes } from 'react'; -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import FRTDiscoverabilityBanner from '..'; - -declare const global: { - wcpaySettings: { - frtDiscoverBannerSettings: string; - lifetimeTPV: number; - }; -}; - -jest.mock( '@woocommerce/components', () => { - const Pill: React.FC< HTMLAttributes< HTMLDivElement > > = ( { - className, - children, - } ) => { children }; - - return { Pill }; -} ); - -jest.mock( '@wordpress/data', () => ( { - useDispatch: jest.fn( () => ( { updateOptions: jest.fn() } ) ), -} ) ); - -describe( 'FRTDiscoverabilityBanner', () => { - beforeEach( () => { - global.wcpaySettings = { - frtDiscoverBannerSettings: '', - lifetimeTPV: 100, - }; - } ); - - it( 'renders', () => { - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); - - it( 'does not render when dontShowAgain is true', () => { - global.wcpaySettings = { - frtDiscoverBannerSettings: JSON.stringify( { - dontShowAgain: true, - } ), - lifetimeTPV: 100, - }; - - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); - - it( 'does not render when no transactions are processed', () => { - global.wcpaySettings = { - frtDiscoverBannerSettings: JSON.stringify( { - dontShowAgain: false, - } ), - lifetimeTPV: 0, - }; - - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); -} ); diff --git a/client/components/payment-method-details/index.js b/client/components/payment-method-details/index.js index 42b94e1461c..fa1299ef687 100755 --- a/client/components/payment-method-details/index.js +++ b/client/components/payment-method-details/index.js @@ -7,7 +7,8 @@ import { Fragment } from 'react'; import './style.scss'; import p24BankList from '../../payment-details/payment-method/p24/bank-list'; import { HoverTooltip } from '../tooltip'; -import { PAYMENT_METHOD_TITLES } from 'wcpay/constants/payment-method'; +import { getTransactionPaymentMethodTitle } from 'wcpay/transactions/utils/getTransactionPaymentMethodTitle'; + /** * * @param {Object} payment Payment charge object @@ -15,6 +16,15 @@ import { PAYMENT_METHOD_TITLES } from 'wcpay/constants/payment-method'; */ const formatDetails = ( payment ) => { const paymentMethod = payment[ payment.type ]; + /** + * FLAG: PAYMENT_METHODS_LIST + * + * When adding a payment method, if you need to display a specific detail, you can + * add it here. If not, you don't need to list it here. + * + * If you're removing a payment method, you'll probably want to leave this section + * section alone because we still need to display the details of existing transactions. + */ switch ( payment.type ) { case 'card': case 'au_becs_debit': @@ -43,12 +53,6 @@ const formatDetails = ( payment ) => { { paymentMethod.iban_last4 } ); - case 'alipay': - case 'affirm': - case 'afterpay_clearpay': - case 'klarna': - case 'multibanco': - case 'wechat_pay': default: return ; } @@ -71,16 +75,22 @@ const PaymentMethodDetails = ( props ) => { } const details = formatDetails( payment ); + + const accountCountry = wcpaySettings?.accountStatus?.country || 'US'; + return ( { details } diff --git a/client/components/payment-method-details/test/__snapshots__/index.js.snap b/client/components/payment-method-details/test/__snapshots__/index.js.snap index 72a69b2596d..6123185fa64 100755 --- a/client/components/payment-method-details/test/__snapshots__/index.js.snap +++ b/client/components/payment-method-details/test/__snapshots__/index.js.snap @@ -22,7 +22,7 @@ exports[`PaymentMethodDetails renders a valid card brand and last 4 digits 1`] = >
diff --git a/client/components/payment-method-details/test/index.js b/client/components/payment-method-details/test/index.js index bfff2311bf2..388a945275c 100755 --- a/client/components/payment-method-details/test/index.js +++ b/client/components/payment-method-details/test/index.js @@ -9,6 +9,12 @@ import { render } from '@testing-library/react'; */ import PaymentMethodDetails from '..'; +global.wcpaySettings = { + accountStatus: { + country: 'US', + }, +}; + describe( 'PaymentMethodDetails', () => { test( 'renders a valid card brand and last 4 digits', () => { const { container: paymentMethodDetails } = renderCard( { diff --git a/client/components/payment-method-disabled-tooltip/index.tsx b/client/components/payment-method-disabled-tooltip/index.tsx index 6ff8a269972..1ac07516327 100644 --- a/client/components/payment-method-disabled-tooltip/index.tsx +++ b/client/components/payment-method-disabled-tooltip/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; * Internal dependencies */ import { HoverTooltip } from 'components/tooltip'; -import PAYMENT_METHOD_IDS from 'wcpay/constants/payment-method'; export const DocumentationUrlForDisabledPaymentMethod = { DEFAULT: @@ -22,17 +21,14 @@ export const DocumentationUrlForDisabledPaymentMethod = { export const getDocumentationUrlForDisabledPaymentMethod = ( paymentMethodId: string ): string => { - let url; - switch ( paymentMethodId ) { - case PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY: - case PAYMENT_METHOD_IDS.AFFIRM: - case PAYMENT_METHOD_IDS.KLARNA: - url = DocumentationUrlForDisabledPaymentMethod.BNPLS; - break; - default: - url = DocumentationUrlForDisabledPaymentMethod.DEFAULT; + const paymentMethodConfig = + window.wooPaymentsPaymentMethodsConfig?.[ paymentMethodId ]; + + if ( paymentMethodConfig?.isBnpl ) { + return DocumentationUrlForDisabledPaymentMethod.BNPLS; } - return url; + + return DocumentationUrlForDisabledPaymentMethod.DEFAULT; }; const PaymentMethodDisabledTooltip = ( { diff --git a/client/components/payment-method-disabled-tooltip/test/index.test.tsx b/client/components/payment-method-disabled-tooltip/test/index.test.tsx index 6f3974d1ce0..bf06ee5b99b 100644 --- a/client/components/payment-method-disabled-tooltip/test/index.test.tsx +++ b/client/components/payment-method-disabled-tooltip/test/index.test.tsx @@ -8,16 +8,39 @@ import React from 'react'; /** * Internal dependencies */ -import PAYMENT_METHOD_IDS from 'wcpay/constants/payment-method'; import PaymentMethodDisabledTooltip, { DocumentationUrlForDisabledPaymentMethod, getDocumentationUrlForDisabledPaymentMethod, } from '../index'; +const mockWooPaymentsPaymentMethodsConfig: typeof window.wooPaymentsPaymentMethodsConfig = { + mock_payment_method_id: { + isBnpl: true, // This is the only property that matters for these tests. + title: 'Mock Payment Method', + icon: '', + darkIcon: '', + countries: [], + testingInstructions: '', + isReusable: false, + showSaveOption: false, + forceNetworkSavedCards: false, + }, +}; + +// Set up the global configuration before tests +beforeAll( () => { + window.wooPaymentsPaymentMethodsConfig = mockWooPaymentsPaymentMethodsConfig; +} ); + +// Clean up after tests +afterAll( () => { + delete window.wooPaymentsPaymentMethodsConfig; +} ); + describe( 'PaymentMethodDisabledTooltip', () => { test.each( [ [ - PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY, + 'mock_payment_method_id', DocumentationUrlForDisabledPaymentMethod.BNPLS, ], [ 'default-method', DocumentationUrlForDisabledPaymentMethod.DEFAULT ], diff --git a/client/components/payment-method-logos/index.tsx b/client/components/payment-method-logos/index.tsx index 7f27c04f82f..d4b73903b64 100644 --- a/client/components/payment-method-logos/index.tsx +++ b/client/components/payment-method-logos/index.tsx @@ -31,6 +31,11 @@ import Przelewy24 from 'assets/images/payment-methods/przelewy24.svg?asset'; import WeChatPay from 'assets/images/payment-method-icons/wechat-pay.svg?asset'; import './style.scss'; +/** + * FLAG: PAYMENT_METHODS_LIST + * If you're adding a new payment method that needs to be displayed on the + * connect account page, you'll need to add it here. + */ const PaymentMethods = [ { name: 'visa', diff --git a/client/constants/payment-method.ts b/client/constants/payment-method.ts index 0fef30e34ee..e5c778e0e26 100644 --- a/client/constants/payment-method.ts +++ b/client/constants/payment-method.ts @@ -3,64 +3,59 @@ */ import { __ } from '@wordpress/i18n'; +/** + * FLAG: PAYMENT_METHODS_LIST + * New payment methods should be added here, the value should match the definition ID. + */ enum PAYMENT_METHOD_IDS { AFFIRM = 'affirm', - ALIPAY = 'alipay', AFTERPAY_CLEARPAY = 'afterpay_clearpay', + ALIPAY = 'alipay', AU_BECS_DEBIT = 'au_becs_debit', BANCONTACT = 'bancontact', CARD = 'card', CARD_PRESENT = 'card_present', EPS = 'eps', - KLARNA = 'klarna', - GRABPAY = 'grabpay', GIROPAY = 'giropay', + GRABPAY = 'grabpay', IDEAL = 'ideal', + KLARNA = 'klarna', LINK = 'link', + MULTIBANCO = 'multibanco', P24 = 'p24', SEPA_DEBIT = 'sepa_debit', SOFORT = 'sofort', - MULTIBANCO = 'multibanco', WECHAT_PAY = 'wechat_pay', } -const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US'; -// This constant is used for rendering tooltip titles for payment methods in transaction list and details pages. +export enum PAYMENT_METHOD_BRANDS { + AMEX = 'amex', + CARTES_BANCAIRES = 'cartes_bancaires', + DINERS = 'diners', + DISCOVER = 'discover', + JCB = 'jcb', + MASTERCARD = 'mastercard', + UNIONPAY = 'unionpay', + VISA = 'visa', +} + +// This constant is used for rendering tooltip titles for "payment methods" in transaction list and details pages. // eslint-disable-next-line @typescript-eslint/naming-convention -export const PAYMENT_METHOD_TITLES = { +export const TRANSACTION_PAYMENT_METHOD_TITLES = { ach_credit_transfer: __( 'ACH Credit Transfer', 'woocommerce-payments' ), ach_debit: __( 'ACH Debit', 'woocommerce-payments' ), acss_debit: __( 'ACSS Debit', 'woocommerce-payments' ), - alipay: __( 'Alipay', 'woocommerce-payments' ), - affirm: __( 'Affirm', 'woocommerce-payments' ), - afterpay_clearpay: - 'GB' === accountCountry - ? __( 'Clearpay', 'woocommerce-payments' ) - : __( 'Afterpay', 'woocommerce-payments' ), amex: __( 'American Express', 'woocommerce-payments' ), - au_becs_debit: __( 'AU BECS Debit', 'woocommerce-payments' ), - bancontact: __( 'Bancontact', 'woocommerce-payments' ), card: __( 'Card Payment', 'woocommerce-payments' ), card_present: __( 'In-Person Card Payment', 'woocommerce-payments' ), cartes_bancaires: __( 'Cartes Bancaires', 'woocommerce-payments' ), diners: __( 'Diners Club', 'woocommerce-payments' ), discover: __( 'Discover', 'woocommerce-payments' ), - eps: __( 'EPS', 'woocommerce-payments' ), - giropay: __( 'giropay', 'woocommerce-payments' ), - ideal: __( 'iDEAL', 'woocommerce-payments' ), jcb: __( 'JCB', 'woocommerce-payments' ), - klarna: __( 'Klarna', 'woocommerce-payments' ), - grabpay: __( 'GrabPay', 'woocommerce-payments' ), - link: __( 'Link', 'woocommerce-payments' ), mastercard: __( 'Mastercard', 'woocommerce-payments' ), - multibanco: __( 'Multibanco', 'woocommerce-payments' ), - p24: __( 'P24', 'woocommerce-payments' ), - sepa_debit: __( 'SEPA Debit', 'woocommerce-payments' ), - sofort: __( 'SOFORT', 'woocommerce-payments' ), stripe_account: __( 'Stripe Account', 'woocommerce-payments' ), unionpay: __( 'Union Pay', 'woocommerce-payments' ), visa: __( 'Visa', 'woocommerce-payments' ), - wechat_pay: __( 'WeChat Pay', 'woocommerce-payments' ), }; export default PAYMENT_METHOD_IDS; diff --git a/client/data/payment-intents/test/hooks.ts b/client/data/payment-intents/test/hooks.ts index c57f8637597..169cf5ace4a 100644 --- a/client/data/payment-intents/test/hooks.ts +++ b/client/data/payment-intents/test/hooks.ts @@ -10,7 +10,11 @@ import { useSelect } from '@wordpress/data'; */ import { usePaymentIntentWithChargeFallback } from '../'; import { STORE_NAME } from '../../constants'; -import { Charge, OutcomeRiskLevel } from '../../../types/charges'; +import { + Charge, + OutcomeRiskLevel, + PaymentMethodDetails, +} from '../../../types/charges'; import { PaymentIntent } from '../../../types/payment-intents'; jest.mock( '@wordpress/data' ); @@ -23,10 +27,10 @@ export const chargeMock: Charge = { id: chargeId, amount: 8903, created: 1656701170, - payment_method_details: { + payment_method_details: ( { card: {}, type: 'card', - }, + } as unknown ) as PaymentMethodDetails, payment_method: 'pm_mock', amount_captured: 8903, amount_refunded: 8903, diff --git a/client/data/settings/actions.js b/client/data/settings/actions.js index 1775eaa71d7..fe5b2414b14 100644 --- a/client/data/settings/actions.js +++ b/client/data/settings/actions.js @@ -5,6 +5,7 @@ */ import { dispatch, select } from '@wordpress/data'; import { apiFetch } from '@wordpress/data-controls'; +import directApiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; /** @@ -281,3 +282,15 @@ export function* submitStripeBillingSubscriptionMigration() { 'scheduleStripeBillingMigration' ); } + +export function saveOption( optionName, value ) { + directApiFetch( { + path: `${ NAMESPACE }/settings/${ optionName }`, + method: 'post', + data: { value }, + } ).catch( () => { + dispatch( 'core/notices' ).createErrorNotice( + __( 'Error saving option', 'woocommerce-payments' ) + ); + } ); +} diff --git a/client/data/transactions/hooks.ts b/client/data/transactions/hooks.ts index 0fb1de6dbc3..53f23d1a4aa 100644 --- a/client/data/transactions/hooks.ts +++ b/client/data/transactions/hooks.ts @@ -11,6 +11,9 @@ import type { Query } from '@woocommerce/navigation'; */ import { STORE_NAME } from '../constants'; import type { DepositStatus } from 'wcpay/types/deposits'; +import PAYMENT_METHOD_IDS, { + PAYMENT_METHOD_BRANDS, +} from 'wcpay/constants/payment-method'; export type TransactionType = | 'charge' @@ -19,6 +22,14 @@ export type TransactionType = | 'financing_payout' | 'financing_paydown'; +export type TransactionSource = + | 'ach_credit_transfer' + | 'ach_debit' + | 'acss_debit' + | 'stripe_account' + | typeof PAYMENT_METHOD_IDS[ keyof typeof PAYMENT_METHOD_IDS ] + | typeof PAYMENT_METHOD_BRANDS[ keyof typeof PAYMENT_METHOD_BRANDS ]; + // TODO: refine this type with more detailed information. export interface Transaction { amount: number; @@ -44,34 +55,7 @@ export interface Transaction { // Usually last 4 digits for card payments, bank name for bank transfers... source_identifier: string; source_device?: string; - source: - | 'ach_credit_transfer' - | 'ach_debit' - | 'acss_debit' - | 'affirm' - | 'afterpay_clearpay' - | 'alipay' - | 'amex' - | 'au_becs_debit' - | 'bancontact' - | 'diners' - | 'discover' - | 'eps' - | 'giropay' - | 'ideal' - | 'jcb' - | 'klarna' - | 'grabpay' - | 'link' - | 'mastercard' - | 'multibanco' - | 'p24' - | 'sepa_debit' - | 'sofort' - | 'stripe_account' - | 'unionpay' - | 'visa' - | 'wechat_pay'; + source: TransactionSource; loan_id?: string; metadata?: { charge_type: 'card_reader_fee'; diff --git a/client/deposits/index.tsx b/client/deposits/index.tsx index cc8aeb4b51d..d54cfc8b81d 100644 --- a/client/deposits/index.tsx +++ b/client/deposits/index.tsx @@ -4,7 +4,6 @@ * External dependencies */ import React, { useState } from 'react'; -import { useDispatch } from '@wordpress/data'; import { ExternalLink } from '@wordpress/components'; import { addQueryArgs } from '@wordpress/url'; @@ -23,9 +22,9 @@ import DepositsList from './list'; import { hasAutomaticScheduledDeposits } from 'wcpay/deposits/utils'; import { recordEvent } from 'wcpay/tracks'; import { MaybeShowMerchantFeedbackPrompt } from 'wcpay/merchant-feedback-prompt'; +import { saveOption } from 'wcpay/data/settings/actions'; const useNextDepositNoticeState = () => { - const { updateOptions } = useDispatch( 'wc/admin/options' ); const [ isDismissed, setIsDismissed ] = useState( wcpaySettings.isNextDepositNoticeDismissed ); @@ -33,9 +32,7 @@ const useNextDepositNoticeState = () => { const setNextDepositNoticeDismissed = () => { setIsDismissed( true ); wcpaySettings.isNextDepositNoticeDismissed = true; - updateOptions( { - wcpay_next_deposit_notice_dismissed: true, - } ); + saveOption( 'wcpay_next_deposit_notice_dismissed', true ); }; return { diff --git a/client/express-checkout/button-ui.js b/client/express-checkout/button-ui.js index 9e29ea6b7b5..32613902faa 100644 --- a/client/express-checkout/button-ui.js +++ b/client/express-checkout/button-ui.js @@ -32,17 +32,16 @@ const expressCheckoutButtonUi = { renderButton: ( eceButton ) => { if ( get$Container()?.length ) { - expressCheckoutButtonUi.showContainer(); eceButton.mount( expressCheckoutElementId ); } }, hideContainer: () => { - get$Container().hide(); + get$Container().removeClass( 'is-ready' ).hide(); }, showContainer: () => { - get$Container().show(); + get$Container().addClass( 'is-ready' ).show(); }, }; diff --git a/client/globals.d.ts b/client/globals.d.ts index 36e76e188f3..80ae10ca2e0 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -114,7 +114,6 @@ declare global { dismissedDuplicateNotices: PaymentMethodToPluginsMap; accountDefaultCurrency: string; isFRTReviewFeatureActive: boolean; - frtDiscoverBannerSettings: string; onboardingFieldsData?: { business_types: Country[]; mccs_display_tree: MccsDisplayTreeItem[]; @@ -172,17 +171,22 @@ declare global { }; const wooPaymentsPaymentMethodDefinitions: Record< + string, + PaymentMethodServerDefinition + >; + + const wooPaymentsPaymentMethodsConfig: Record< string, { - id: string; - stripe_key: string; + isReusable: boolean; + isBnpl: boolean; title: string; - description: string; - settings_icon_url: string; - currencies: string[]; - allows_manual_capture: boolean; - allows_pay_later: boolean; - accepts_only_domestic_payment: boolean; + icon: string; + darkIcon: string; + showSaveOption: boolean; + countries: string[]; + testingInstructions: string; + forceNetworkSavedCards: boolean; } >; @@ -275,5 +279,6 @@ declare global { wcTracks: typeof wcTracks; wcSettings: typeof wcSettings; wcpayPluginSettings?: typeof wcpayPluginSettings; + wooPaymentsPaymentMethodsConfig?: typeof wooPaymentsPaymentMethodsConfig; } } diff --git a/client/onboarding/index.tsx b/client/onboarding/index.tsx index ccf0137afa6..3953d3282ed 100644 --- a/client/onboarding/index.tsx +++ b/client/onboarding/index.tsx @@ -65,7 +65,7 @@ const getComingSoonShareKey = () => { const initialData = { business_name: wcSettings?.siteTitle, mcc: getMccFromIndustry(), - url: + site: location.hostname === 'localhost' ? 'https://wcpay.test' : wcSettings?.homeUrl + getComingSoonShareKey(), diff --git a/client/overview/index.js b/client/overview/index.js index 2bb1836e258..8a584569105 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -20,7 +20,6 @@ import ActiveLoanSummary from 'components/active-loan-summary'; import ConnectionSuccessModal from './modal/connection-success'; import DepositsOverview from 'components/deposits-overview'; import ErrorBoundary from 'components/error-boundary'; -import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; import JetpackIdcNotice from 'components/jetpack-idc-notice'; import Page from 'components/page'; import PaymentActivity from 'wcpay/components/payment-activity'; @@ -336,10 +335,6 @@ const OverviewPage = () => { } ) } ) } - - - - { ! accountRejected && ! accountUnderReview && ( diff --git a/client/overview/modal/connection-success/index.tsx b/client/overview/modal/connection-success/index.tsx index 752a68fb356..0eec720bbd5 100644 --- a/client/overview/modal/connection-success/index.tsx +++ b/client/overview/modal/connection-success/index.tsx @@ -3,13 +3,13 @@ */ import React from 'react'; import { Modal, Button } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import strings from './strings'; +import { saveOption } from 'wcpay/data/settings/actions'; /** * A modal component displayed when a live account is successfully connected. @@ -18,15 +18,12 @@ export const ConnectionSuccessModal = () => { const [ isDismissed, setIsDismissed ] = React.useState( wcpaySettings.isConnectionSuccessModalDismissed ); - const { updateOptions } = useDispatch( 'wc/admin/options' ); const onDismiss = async () => { setIsDismissed( true ); // Update the option to mark the modal as dismissed. - await updateOptions( { - wcpay_connection_success_modal_dismissed: true, - } ); + saveOption( 'wcpay_connection_success_modal_dismissed', true ); }; return ( diff --git a/client/overview/modal/connection-success/test/index.test.tsx b/client/overview/modal/connection-success/test/index.test.tsx index e4a7d40da1b..bb328cda08a 100644 --- a/client/overview/modal/connection-success/test/index.test.tsx +++ b/client/overview/modal/connection-success/test/index.test.tsx @@ -10,8 +10,8 @@ import user from '@testing-library/user-event'; */ import ConnectionSuccessModal from '../index'; -jest.mock( '@wordpress/data', () => ( { - useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), +jest.mock( 'wcpay/data/settings/actions', () => ( { + saveOption: jest.fn(), } ) ); declare const global: { diff --git a/client/overview/modal/progressive-onboarding-eligibility/index.tsx b/client/overview/modal/progressive-onboarding-eligibility/index.tsx index 21a6e5a22fa..ec954aad109 100644 --- a/client/overview/modal/progressive-onboarding-eligibility/index.tsx +++ b/client/overview/modal/progressive-onboarding-eligibility/index.tsx @@ -6,7 +6,6 @@ import { __, sprintf } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { Button, Modal } from '@wordpress/components'; import { Icon, store, currencyDollar } from '@wordpress/icons'; -import { useDispatch } from '@wordpress/data'; import interpolateComponents from '@automattic/interpolate-components'; /** @@ -14,6 +13,7 @@ import interpolateComponents from '@automattic/interpolate-components'; */ import { trackEligibilityModalClosed } from 'onboarding/tracking'; import ConfettiAnimation from 'components/confetti-animation'; +import { saveOption } from 'wcpay/data/settings/actions'; import './style.scss'; const ProgressiveOnboardingEligibilityModal: React.FC = () => { @@ -22,8 +22,6 @@ const ProgressiveOnboardingEligibilityModal: React.FC = () => { wcpaySettings.progressiveOnboarding?.isEligibilityModalDismissed ); - const { updateOptions } = useDispatch( 'wc/admin/options' ); - const urlParams = new URLSearchParams( window.location.search ); const urlSource = urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown'; @@ -32,9 +30,7 @@ const ProgressiveOnboardingEligibilityModal: React.FC = () => { setModalDismissed( true ); // Update the option to mark the modal as dismissed. - await updateOptions( { - wcpay_onboarding_eligibility_modal_dismissed: true, - } ); + saveOption( 'wcpay_onboarding_eligibility_modal_dismissed', true ); }; const handleSetup = () => { diff --git a/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx b/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx index 7bb30a07131..533497f38ad 100644 --- a/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx +++ b/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx @@ -10,8 +10,8 @@ import user from '@testing-library/user-event'; */ import ProgressiveOnboardingEligibilityModal from '../index'; -jest.mock( '@wordpress/data', () => ( { - useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), +jest.mock( 'wcpay/data/settings/actions', () => ( { + saveOption: jest.fn(), } ) ); declare const global: { diff --git a/client/overview/task-list/index.js b/client/overview/task-list/index.js index 6bb00304c0d..84ffbe8f02d 100644 --- a/client/overview/task-list/index.js +++ b/client/overview/task-list/index.js @@ -13,10 +13,10 @@ import { useCallback, useEffect, useState } from '@wordpress/element'; * Internal dependencies */ import { TIME } from 'wcpay/constants/time'; +import { saveOption } from 'wcpay/data/settings/actions'; const TaskList = ( { overviewTasksVisibility, tasks } ) => { const { createNotice } = useDispatch( 'core/notices' ); - const { updateOptions } = useDispatch( 'wc/admin/options' ); const [ visibleTasks, setVisibleTasks ] = useState( tasks ); const { deletedTodoTasks, @@ -52,9 +52,7 @@ const TaskList = ( { overviewTasksVisibility, tasks } ) => { dismissedTasks.splice( dismissedTodoTasks.indexOf( key ), 1 ); setVisibleTasks( getVisibleTasks() ); - await updateOptions( { - [ optionName ]: updatedDismissedTasks, - } ); + saveOption( optionName, updatedDismissedTasks ); }; const dismissSelectedTask = async ( { @@ -68,9 +66,7 @@ const TaskList = ( { overviewTasksVisibility, tasks } ) => { dismissedTasks.push( key ); setVisibleTasks( getVisibleTasks() ); - await updateOptions( { - [ optionName ]: [ ...dismissedTasks ], - } ); + saveOption( optionName, [ ...dismissedTasks ] ); createNotice( 'success', noticeMessage, { actions: [ @@ -120,9 +116,10 @@ const TaskList = ( { overviewTasksVisibility, tasks } ) => { delete remindMeLaterTodoTasks[ key ]; setVisibleTasks( getVisibleTasks() ); - await updateOptions( { - woocommerce_remind_me_later_todo_tasks: updatedRemindMeLaterTasks, - } ); + saveOption( + 'woocommerce_remind_me_later_todo_tasks', + updatedRemindMeLaterTasks + ); }; const remindTaskLater = async ( { key, onDismiss } ) => { @@ -130,11 +127,9 @@ const TaskList = ( { overviewTasksVisibility, tasks } ) => { remindMeLaterTodoTasks[ key ] = dismissTime; setVisibleTasks( getVisibleTasks() ); - await updateOptions( { - woocommerce_remind_me_later_todo_tasks: { - ...remindMeLaterTodoTasks, - [ key ]: dismissTime, - }, + saveOption( 'woocommerce_remind_me_later_todo_tasks', { + ...remindMeLaterTodoTasks, + [ key ]: dismissTime, } ); createNotice( diff --git a/client/overview/test/index.js b/client/overview/test/index.js index efc3958cd98..6067f9774d8 100644 --- a/client/overview/test/index.js +++ b/client/overview/test/index.js @@ -3,7 +3,7 @@ /** * External dependencies */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { select } from '@wordpress/data'; /** @@ -12,7 +12,6 @@ import { select } from '@wordpress/data'; import OverviewPage from '../'; import { getTasks } from '../task-list/tasks'; import { getQuery } from '@woocommerce/navigation'; -import userEvent from '@testing-library/user-event'; const settingsMock = { enabled_payment_method_ids: [ 'foo', 'bar' ], @@ -113,11 +112,6 @@ describe( 'Overview page', () => { accountOverviewTaskList: true, }, accountLoans: {}, - frtDiscoverBannerSettings: JSON.stringify( { - remindMeCount: 0, - remindMeAt: null, - dontShowAgain: false, - } ), }; getQuery.mockReturnValue( {} ); getTasks.mockReturnValue( [] ); @@ -261,60 +255,6 @@ describe( 'Overview page', () => { ).toBeVisible(); } ); - it( 'renders FRTDiscoverabilityBanner if store has transacted', () => { - global.wcpaySettings = { - ...global.wcpaySettings, - frtDiscoverBannerSettings: JSON.stringify( { - dontShowAgain: false, - } ), - lifetimeTPV: 100, - }; - render( ); - - expect( - screen.queryByText( 'Enhanced fraud protection for your store' ) - ).toBeInTheDocument(); - } ); - - it( 'does not render FRTDiscoverabilityBanner if store has not transacted', () => { - global.wcpaySettings = { - ...global.wcpaySettings, - frtDiscoverBannerSettings: JSON.stringify( { - dontShowAgain: false, - } ), - lifetimeTPV: 0, - }; - render( ); - - expect( - screen.queryByText( 'Enhanced fraud protection for your store' ) - ).not.toBeInTheDocument(); - } ); - - it( 'dismisses the FRTDiscoverabilityBanner when dismiss button is clicked', async () => { - global.wcpaySettings = { - ...global.wcpaySettings, - frtDiscoverBannerSettings: JSON.stringify( { - dontShowAgain: false, - } ), - lifetimeTPV: 100, - }; - - render( ); - - const bannerHeader = screen.getByText( - 'Enhanced fraud protection for your store' - ); - - expect( bannerHeader ).toBeInTheDocument(); - - userEvent.click( screen.getByText( 'Dismiss' ) ); - - await waitFor( () => { - expect( bannerHeader ).not.toBeInTheDocument(); - } ); - } ); - it( 'displays ProgressiveOnboardingEligibilityModal if showProgressiveOnboardingEligibilityModal is true', () => { getQuery.mockReturnValue( { 'wcpay-connection-success': '1' } ); diff --git a/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap index a47a9d9bc8e..fae05e56100 100644 --- a/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap @@ -238,7 +238,7 @@ exports[`Order details page should match the snapshot - Charge without payment i >
diff --git a/client/payment-details/order-details/test/index.test.tsx b/client/payment-details/order-details/test/index.test.tsx index af7bbd12275..bb1254503a3 100644 --- a/client/payment-details/order-details/test/index.test.tsx +++ b/client/payment-details/order-details/test/index.test.tsx @@ -19,6 +19,9 @@ import { ApiError } from 'wcpay/types/errors'; declare const global: { wcSettings: { countries: Record< string, string > }; wcpaySettings: { + accountStatus: { + country: string; + }; zeroDecimalCurrencies: string[]; featureFlags: Record< string, boolean >; connect: { @@ -148,6 +151,9 @@ describe( 'Order details page', () => { }; global.wcpaySettings = { + accountStatus: { + country: 'US', + }, featureFlags: { paymentTimeline: true }, zeroDecimalCurrencies: [], connect: { country: 'US' }, diff --git a/client/payment-details/payment-method/index.js b/client/payment-details/payment-method/index.js index 9388b1fac50..6a3b95747df 100644 --- a/client/payment-details/payment-method/index.js +++ b/client/payment-details/payment-method/index.js @@ -28,6 +28,10 @@ import SofortDetails from './sofort'; import MultibancoDetails from './multibanco'; import WeChatPayDetails from './wechat-pay'; +/** + * FLAG: PAYMENT_METHODS_LIST + * There is some duplicated code in these detailed components that needs to be spiked on for a refactor. + */ const detailsComponentMap = { affirm: AffirmDetails, alipay: AlipayDetails, diff --git a/client/payment-details/summary/__tests__/__snapshots__/index.test.js.snap b/client/payment-details/summary/__tests__/__snapshots__/index.test.js.snap index c964a4079d4..1be02b73ba2 100644 --- a/client/payment-details/summary/__tests__/__snapshots__/index.test.js.snap +++ b/client/payment-details/summary/__tests__/__snapshots__/index.test.js.snap @@ -247,7 +247,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca >
@@ -603,7 +603,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th >
@@ -962,7 +962,7 @@ exports[`PaymentDetailsSummary correctly renders a charge 1`] = ` > @@ -1272,7 +1272,7 @@ exports[`PaymentDetailsSummary correctly renders when payment intent is missing > @@ -1596,7 +1596,7 @@ exports[`PaymentDetailsSummary order missing notice does not render notice if or > @@ -1917,7 +1917,7 @@ exports[`PaymentDetailsSummary order missing notice renders notice if order miss > @@ -2294,7 +2294,7 @@ exports[`PaymentDetailsSummary renders a charge with subscriptions 1`] = ` > @@ -2596,7 +2596,7 @@ exports[`PaymentDetailsSummary renders fully refunded information for a charge 1 > @@ -3167,7 +3167,7 @@ exports[`PaymentDetailsSummary renders partially refunded information for a char > @@ -3491,7 +3491,7 @@ exports[`PaymentDetailsSummary renders the Tap to Pay channel from metadata 1`] > @@ -3815,7 +3815,7 @@ exports[`PaymentDetailsSummary renders the information of a dispute-reversal cha > diff --git a/client/payment-details/summary/__tests__/index.test.js b/client/payment-details/summary/__tests__/index.test.js index 8daed75e2ab..fa05706216d 100755 --- a/client/payment-details/summary/__tests__/index.test.js +++ b/client/payment-details/summary/__tests__/index.test.js @@ -157,6 +157,9 @@ describe( 'PaymentDetailsSummary', () => { jest.clearAllMocks(); global.wcpaySettings = { + accountStatus: { + country: 'US', + }, isSubscriptionsActive: false, shouldUseExplicitPrice: false, zeroDecimalCurrencies: [ 'jpy' ], diff --git a/client/payment-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/test/__snapshots__/index.test.tsx.snap index db5e21518a7..cd91fa5cd67 100644 --- a/client/payment-details/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/test/__snapshots__/index.test.tsx.snap @@ -747,7 +747,7 @@ exports[`Payment details page should match the snapshot - Payment Intent query p > diff --git a/client/payment-details/test/index.test.tsx b/client/payment-details/test/index.test.tsx index ffb842aa84b..175388c8a0f 100644 --- a/client/payment-details/test/index.test.tsx +++ b/client/payment-details/test/index.test.tsx @@ -15,6 +15,9 @@ import PaymentDetailsPage from '..'; declare const global: { wcSettings: { countries: Record< string, string > }; wcpaySettings: { + accountStatus: { + country: string; + }; zeroDecimalCurrencies: string[]; featureFlags: Record< string, boolean >; connect: { @@ -147,6 +150,9 @@ global.wcSettings = { }; global.wcpaySettings = { + accountStatus: { + country: 'US', + }, featureFlags: { paymentTimeline: true }, zeroDecimalCurrencies: [ 'usd' ], connect: { country: 'US' }, diff --git a/client/payment-methods-icons.tsx b/client/payment-methods-icons.tsx index 46eb21e10b2..5a1d4359282 100644 --- a/client/payment-methods-icons.tsx +++ b/client/payment-methods-icons.tsx @@ -8,23 +8,9 @@ import classNames from 'classnames'; /** * Internal dependencies */ -import BancontactAsset from 'assets/images/payment-methods/bancontact.svg?asset'; -import EpsAsset from 'assets/images/payment-methods/eps.svg?asset'; -import GiropayAsset from 'assets/images/payment-methods/giropay.svg?asset'; -import SofortAsset from 'assets/images/payment-methods/sofort.svg?asset'; -import SepaAsset from 'assets/images/payment-methods/sepa-debit.svg?asset'; -import P24Asset from 'assets/images/payment-methods/p24.svg?asset'; -import IdealAsset from 'assets/images/payment-methods/ideal.svg?asset'; -import BankDebitAsset from 'assets/images/payment-methods/bank-debit.svg?asset'; -import AffirmAsset from 'assets/images/payment-methods/affirm-badge.svg?asset'; -import AfterpayAsset from 'assets/images/payment-methods/afterpay-logo.svg?asset'; -import ClearpayAsset from 'assets/images/payment-methods/clearpay.svg?asset'; import JCBAsset from 'assets/images/payment-methods/jcb.svg?asset'; -import KlarnaAsset from 'assets/images/payment-methods/klarna.svg?asset'; -import GrabPayAsset from 'assets/images/payment-methods/grabpay.svg?asset'; import VisaAsset from 'assets/images/cards/visa.svg?asset'; import MasterCardAsset from 'assets/images/cards/mastercard.svg?asset'; -import MultibancoAsset from 'assets/images/payment-methods/multibanco.svg?asset'; import AmexAsset from 'assets/images/cards/amex.svg?asset'; import WooAsset from 'assets/images/payment-methods/woo.svg?asset'; import WooAssetShort from 'assets/images/payment-methods/woo-short.svg?asset'; @@ -35,8 +21,6 @@ import DiscoverAsset from 'assets/images/cards/discover.svg?asset'; import CBAsset from 'assets/images/cards/cb.svg?asset'; import UnionPayAsset from 'assets/images/cards/unionpay.svg?asset'; import LinkAsset from 'assets/images/payment-methods/link.svg?asset'; -import CreditCardAsset from 'assets/images/payment-methods/cc.svg?asset'; -import WeChatPayAsset from 'assets/images/payment-methods/wechat-pay.svg?asset'; import './style.scss'; const iconComponent = ( @@ -56,18 +40,6 @@ const iconComponent = ( /> ); -export const AffirmIcon = iconComponent( - AffirmAsset, - __( 'Affirm', 'woocommerce-payments' ) -); -export const AfterpayIcon = iconComponent( - AfterpayAsset, - __( 'Afterpay', 'woocommerce-payments' ) -); -export const ClearpayIcon = iconComponent( - ClearpayAsset, - __( 'Clearpay', 'woocommerce-payments' ) -); export const AmericanExpressIcon = iconComponent( AmexAsset, __( 'American Express', 'woocommerce-payments' ) @@ -76,19 +48,6 @@ export const ApplePayIcon = iconComponent( ApplePayAsset, __( 'Apple Pay', 'woocommerce-payments' ) ); -export const BancontactIcon = iconComponent( - BancontactAsset, - __( 'Bancontact', 'woocommerce-payments' ) -); -export const BankDebitIcon = iconComponent( - BankDebitAsset, - __( 'BECS Direct Debit', 'woocommerce-payments' ) -); -export const CreditCardIcon = iconComponent( - CreditCardAsset, - __( 'Credit card / Debit card', 'woocommerce-payments' ), - false -); export const CBIcon = iconComponent( CBAsset, __( 'Cartes Bancaires', 'woocommerce-payments' ) @@ -101,30 +60,14 @@ export const DiscoverIcon = iconComponent( DiscoverAsset, __( 'Discover', 'woocommerce-payments' ) ); -export const EpsIcon = iconComponent( - EpsAsset, - __( 'BECS Direct Debit', 'woocommerce-payments' ) -); -export const GiropayIcon = iconComponent( - GiropayAsset, - __( 'Giropay', 'woocommerce-payments' ) -); export const GooglePayIcon = iconComponent( GooglePayAsset, __( 'Google Pay', 'woocommerce-payments' ) ); -export const IdealIcon = iconComponent( - IdealAsset, - __( 'iDEAL', 'woocommerce-payments' ) -); export const JCBIcon = iconComponent( JCBAsset, __( 'JCB', 'woocommerce-payments' ) ); -export const KlarnaIcon = iconComponent( - KlarnaAsset, - __( 'Klarna', 'woocommerce-payments' ) -); export const LinkIcon = iconComponent( LinkAsset, __( 'Link', 'woocommerce-payments' ) @@ -133,22 +76,6 @@ export const MastercardIcon = iconComponent( MasterCardAsset, __( 'Mastercard', 'woocommerce-payments' ) ); -export const MultibancoIcon = iconComponent( - MultibancoAsset, - __( 'Multibanco', 'woocommerce-payments' ) -); -export const P24Icon = iconComponent( - P24Asset, - __( 'Przelewy24 (P24)', 'woocommerce-payments' ) -); -export const SepaIcon = iconComponent( - SepaAsset, - __( 'SEPA Direct Debit', 'woocommerce-payments' ) -); -export const SofortIcon = iconComponent( - SofortAsset, - __( 'Sofort', 'woocommerce-payments' ) -); export const UnionPayIcon = iconComponent( UnionPayAsset, __( 'UnionPay', 'woocommerce-payments' ) @@ -157,14 +84,6 @@ export const VisaIcon = iconComponent( VisaAsset, __( 'Visa', 'woocommerce-payments' ) ); -export const GrabPayIcon = iconComponent( - GrabPayAsset, - __( 'GrabPay', 'woocommerce-payments' ) -); -export const WeChatPayIcon = iconComponent( - WeChatPayAsset, - __( 'WeChat Pay', 'woocommerce-payments' ) -); export const WooIcon = iconComponent( WooAsset, __( 'WooPay', 'woocommerce-payments' ), diff --git a/client/payment-methods-map.tsx b/client/payment-methods-map.tsx index 07206384353..d1e081f1a1a 100644 --- a/client/payment-methods-map.tsx +++ b/client/payment-methods-map.tsx @@ -9,316 +9,55 @@ import classNames from 'classnames'; * Internal dependencies */ -import { - AffirmIcon, - AfterpayIcon, - ClearpayIcon, - BancontactIcon, - BankDebitIcon, - CreditCardIcon, - EpsIcon, - GiropayIcon, - IdealIcon, - JCBIcon, - KlarnaIcon, - P24Icon, - SepaIcon, - SofortIcon, - MultibancoIcon, - GrabPayIcon, - WeChatPayIcon, -} from 'wcpay/payment-methods-icons'; - -const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US'; +import { JCBIcon } from 'wcpay/payment-methods-icons'; import type { PaymentMethodMapEntry } from './types/payment-methods'; -// Get any payment method definitions from the client. -const PaymentMethodDefinitions = - typeof wooPaymentsPaymentMethodDefinitions !== 'undefined' - ? wooPaymentsPaymentMethodDefinitions - : {}; +if ( typeof wooPaymentsPaymentMethodDefinitions === 'undefined' ) { + throw new Error( 'wooPaymentsPaymentMethodDefinitions is undefined' ); +} -const convertedPaymentMethodDefinitions = Object.fromEntries< +const PaymentMethodInformationObject: Record< + string, PaymentMethodMapEntry ->( - Object.entries( PaymentMethodDefinitions ).map( ( [ key, value ] ) => [ - key, - { - id: value.id, - label: value.title, - description: value.description, - icon: ( { className } ) => ( +> = Object.keys( wooPaymentsPaymentMethodDefinitions ).reduce( + ( acc: Record< string, PaymentMethodMapEntry >, key: string ) => { + acc[ key ] = { + ...wooPaymentsPaymentMethodDefinitions[ key ], + icon: ( { className }: { className?: string } ) => ( { ), - currencies: value.currencies, - stripe_key: value.stripe_key, - allows_manual_capture: value.allows_manual_capture, - allows_pay_later: value.allows_pay_later, - accepts_only_domestic_payment: value.accepts_only_domestic_payment, + }; + return acc; + }, + { + jcb: { + id: 'jcb', + label: __( 'JCB', 'woocommerce-payments' ), + description: __( + 'Let your customers pay with JCB, the only international payment brand based in Japan.', + 'woocommerce-payments' + ), + icon: JCBIcon, + currencies: [ 'JPY' ], + stripe_key: 'jcb_payments', + allows_manual_capture: false, + allows_pay_later: false, + accepts_only_domestic_payment: false, + settings_icon_url: '', }, - ] ) + } ); -const PaymentMethodInformationObject: Record< - string, - PaymentMethodMapEntry -> = { - card: { - id: 'card', - label: __( 'Credit / Debit Cards', 'woocommerce-payments' ), - description: __( - 'Let your customers pay with major credit and debit cards without leaving your store.', - 'woocommerce-payments' - ), - icon: CreditCardIcon, - currencies: [], - stripe_key: 'card_payments', - allows_manual_capture: true, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - au_becs_debit: { - id: 'au_becs_debit', - label: __( 'BECS Direct Debit', 'woocommerce-payments' ), - description: __( - 'Bulk Electronic Clearing System — Accept secure bank transfer from Australia.', - 'woocommerce-payments' - ), - icon: BankDebitIcon, - currencies: [ 'AUD' ], - stripe_key: 'au_becs_debit_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - bancontact: { - id: 'bancontact', - label: __( 'Bancontact', 'woocommerce-payments' ), - description: __( - 'Bancontact is a bank redirect payment method offered by more than 80% of online businesses in Belgium.', - 'woocommerce-payments' - ), - icon: BancontactIcon, - currencies: [ 'EUR' ], - stripe_key: 'bancontact_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - eps: { - id: 'eps', - label: __( 'EPS', 'woocommerce-payments' ), - description: __( - 'Accept your payment with EPS — a common payment method in Austria.', - 'woocommerce-payments' - ), - icon: EpsIcon, - currencies: [ 'EUR' ], - stripe_key: 'eps_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - giropay: { - id: 'giropay', - label: __( 'giropay', 'woocommerce-payments' ), - description: __( - 'Expand your business with giropay — Germany’s second most popular payment system.', - 'woocommerce-payments' - ), - icon: GiropayIcon, - currencies: [ 'EUR' ], - stripe_key: 'giropay_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - ideal: { - id: 'ideal', - label: __( 'iDEAL', 'woocommerce-payments' ), - description: __( - 'Expand your business with iDEAL — Netherlands’s most popular payment method.', - 'woocommerce-payments' - ), - icon: IdealIcon, - currencies: [ 'EUR' ], - stripe_key: 'ideal_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - p24: { - id: 'p24', - label: __( 'Przelewy24 (P24)', 'woocommerce-payments' ), - description: __( - 'Accept payments with Przelewy24 (P24), the most popular payment method in Poland.', - 'woocommerce-payments' - ), - icon: P24Icon, - currencies: [ 'EUR', 'PLN' ], - stripe_key: 'p24_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - sepa_debit: { - id: 'sepa_debit', - label: __( 'SEPA Direct Debit', 'woocommerce-payments' ), - description: __( - 'Reach 500 million customers and over 20 million businesses across the European Union.', - 'woocommerce-payments' - ), - icon: SepaIcon, - currencies: [ 'EUR' ], - stripe_key: 'sepa_debit_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - sofort: { - id: 'sofort', - label: __( 'Sofort', 'woocommerce-payments' ), - description: __( - 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.', - 'woocommerce-payments' - ), - icon: SofortIcon, - currencies: [ 'EUR' ], - stripe_key: 'sofort_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - multibanco: { - id: 'multibanco', - label: __( 'Multibanco', 'woocommerce-payments' ), - description: __( - 'A voucher based payment method for your customers in Portugal.', - 'woocommerce-payments' - ), - icon: MultibancoIcon, - currencies: [ 'EUR' ], - stripe_key: 'multibanco_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - affirm: { - id: 'affirm', - label: __( 'Affirm', 'woocommerce-payments' ), - description: __( - 'Allow customers to pay over time with Affirm.', - 'woocommerce-payments' - ), - icon: AffirmIcon, - currencies: [ 'USD', 'CAD' ], - stripe_key: 'affirm_payments', - allows_manual_capture: false, - allows_pay_later: true, - accepts_only_domestic_payment: true, - }, - afterpay_clearpay: { - id: 'afterpay_clearpay', - label: - 'GB' === accountCountry - ? __( 'Clearpay', 'woocommerce-payments' ) - : __( 'Afterpay', 'woocommerce-payments' ), - description: - 'GB' === accountCountry - ? __( - 'Allow customers to pay over time with Clearpay.', - 'woocommerce-payments' - ) - : __( - 'Allow customers to pay over time with Afterpay.', - 'woocommerce-payments' - ), - icon: 'GB' === accountCountry ? ClearpayIcon : AfterpayIcon, - currencies: [ 'USD', 'AUD', 'CAD', 'NZD', 'GBP' ], - stripe_key: 'afterpay_clearpay_payments', - allows_manual_capture: false, - allows_pay_later: true, - accepts_only_domestic_payment: true, - }, - jcb: { - id: 'jcb', - label: __( 'JCB', 'woocommerce-payments' ), - description: __( - 'Let your customers pay with JCB, the only international payment brand based in Japan.', - 'woocommerce-payments' - ), - icon: JCBIcon, - currencies: [ 'JPY' ], - stripe_key: 'jcb_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - klarna: { - id: 'klarna', - label: __( 'Klarna', 'woocommerce-payments' ), - description: __( - 'Allow customers to pay over time or pay now with Klarna.', - 'woocommerce-payments' - ), - icon: KlarnaIcon, - currencies: [ 'EUR', 'GBP', 'USD', 'DKK', 'NOK', 'SEK' ], - stripe_key: 'klarna_payments', - allows_manual_capture: false, - allows_pay_later: true, - accepts_only_domestic_payment: true, - }, - grabpay: { - id: 'grabpay', - label: __( 'GrabPay', 'woocommerce-payments' ), - description: __( - 'A popular digital wallet for cashless payments in Singapore.', - 'woocommerce-payments' - ), - icon: GrabPayIcon, - currencies: [ 'SGD' ], - stripe_key: 'grabpay_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - wechat_pay: { - id: 'wechat_pay', - label: __( 'WeChat Pay', 'woocommerce-payments' ), - description: __( - 'A digital wallet popular with customers from China.', - 'woocommerce-payments' - ), - icon: WeChatPayIcon, - currencies: [ - 'USD', - 'CNY', - 'AUD', - 'CAD', - 'EUR', - 'GBP', - 'HKD', - 'JPY', - 'SGD', - 'DKK', - 'NOK', - 'SEK', - 'CHF', - ], - stripe_key: 'wechat_pay_payments', - allows_manual_capture: false, - allows_pay_later: false, - accepts_only_domestic_payment: false, - }, - ...convertedPaymentMethodDefinitions, -}; - export default PaymentMethodInformationObject; diff --git a/client/plugins-page/index.js b/client/plugins-page/index.js index b960794f65d..37a514e4b52 100644 --- a/client/plugins-page/index.js +++ b/client/plugins-page/index.js @@ -2,17 +2,15 @@ * External dependencies */ import React, { useState, useEffect, useCallback } from 'react'; -import { useDispatch } from '@wordpress/data'; import ReactDOM from 'react-dom'; -import { OPTIONS_STORE_NAME } from '@woocommerce/data'; /** * Internal dependencies */ import PluginDisableSurvey from './deactivation-survey'; +import { saveOption } from 'wcpay/data/settings/actions'; const PluginsPage = () => { - const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); const [ modalOpen, setModalOpen ] = useState( false ); const surveyModalTimestamp = window.wcpayPluginsSettings?.exitSurveyLastShown ?? null; @@ -41,9 +39,7 @@ const PluginsPage = () => { const currentDate = new Date(); // Update modal dismissed option. - await updateOptions( { - wcpay_exit_survey_last_shown: currentDate, - } ); + saveOption( 'wcpay_exit_survey_last_shown', currentDate ); window.wcpayPluginsSettings.exitSurveyLastShown = currentDate; diff --git a/client/settings/advanced-settings/stripe-billing-section.tsx b/client/settings/advanced-settings/stripe-billing-section.tsx index bd0e3ce270f..57ab56f1a5d 100644 --- a/client/settings/advanced-settings/stripe-billing-section.tsx +++ b/client/settings/advanced-settings/stripe-billing-section.tsx @@ -3,6 +3,8 @@ */ import React, { useState, useEffect } from 'react'; import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; /** * Internal dependencies @@ -11,7 +13,9 @@ import { useStripeBilling, useStripeBillingMigration, useSettings, + useManualCapture, } from 'wcpay/data'; +import ConfirmationModal from 'wcpay/components/confirmation-modal'; import Notices from './stripe-billing-notices/notices'; import StripeBillingMigrationNoticeContext from './stripe-billing-notices/context'; import StripeBillingToggle from './stripe-billing-toggle'; @@ -27,6 +31,7 @@ const StripeBillingSection: React.FC = () => { isStripeBillingEnabled, updateIsStripeBillingEnabled, ] = useStripeBilling() as StripeBillingHook; + const [ isManualCaptureEnabled ] = useManualCapture() as [ boolean ]; const [ isMigrationInProgress, migratedCount, @@ -90,8 +95,21 @@ const StripeBillingSection: React.FC = () => { hasResolvedMigrateRequest: hasResolved, }; + const [ + isStripeBillingManualCaptureConflictModalOpen, + setStripeBillingManualCaptureConflictModalOpen, + ] = useState( false ); + const openStripeBillingManualCaptureConflictModal = () => + setStripeBillingManualCaptureConflictModalOpen( true ); + const closeStripeBillingManualCaptureConflictModal = () => + setStripeBillingManualCaptureConflictModalOpen( false ); + // When the toggle is changed, update the WooPayments settings and reset the hasSavedSettings flag. const stripeBillingSettingToggle = ( enabled: boolean ) => { + if ( enabled && isManualCaptureEnabled ) { + openStripeBillingManualCaptureConflictModal(); + return; + } updateIsStripeBillingEnabled( enabled ); setHasSavedSettings( false ); }; @@ -101,6 +119,41 @@ const StripeBillingSection: React.FC = () => {

{ __( 'Subscriptions', 'woocommerce-payments' ) }

+ { isStripeBillingManualCaptureConflictModalOpen && ( + + + + } + onRequestClose={ + closeStripeBillingManualCaptureConflictModal + } + > +

+ { createInterpolateElement( + __( + 'Stripe Billing is not available with manual capture enabled. To use Stripe Billing, disable manual capture in your settings list.', + 'woocommerce-payments' + ), + { + b: , + } + ) } +

+
+ ) } ); }; diff --git a/client/settings/express-checkout-settings/__tests__/index.test.js b/client/settings/express-checkout-settings/__tests__/index.test.js index c3377aa447c..1334386c1e9 100644 --- a/client/settings/express-checkout-settings/__tests__/index.test.js +++ b/client/settings/express-checkout-settings/__tests__/index.test.js @@ -3,7 +3,7 @@ /** * External dependencies */ -import { render, screen, within } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * Internal dependencies @@ -97,22 +97,6 @@ describe( 'ExpressCheckoutSettings', () => { expect( errorMessage ).toBeInTheDocument(); } ); - test( 'renders payment request breadcrumbs', () => { - renderWithSettingsProvider( - - ); - - const linkToPayments = screen.getByRole( 'link', { - name: 'WooPayments', - } ); - const breadcrumbs = linkToPayments.closest( 'h2' ); - - const methodName = within( breadcrumbs ).getByText( - 'Apple Pay / Google Pay' - ); - expect( breadcrumbs ).toContainElement( methodName ); - } ); - test( 'renders payment request title and description', () => { renderWithSettingsProvider( @@ -147,20 +131,6 @@ describe( 'ExpressCheckoutSettings', () => { ).toBeInTheDocument(); } ); - test( 'renders woopay breadcrumbs', () => { - renderWithSettingsProvider( - - ); - - const linkToPayments = screen.getByRole( 'link', { - name: 'WooPayments', - } ); - const breadcrumbs = linkToPayments.closest( 'h2' ); - - const methodName = within( breadcrumbs ).getByText( 'WooPay' ); - expect( breadcrumbs ).toContainElement( methodName ); - } ); - test( 'renders woopay settings and confirm its checkbox label', () => { renderWithSettingsProvider( diff --git a/client/settings/express-checkout-settings/index.js b/client/settings/express-checkout-settings/index.js index b55628c522a..9dc382f77bc 100644 --- a/client/settings/express-checkout-settings/index.js +++ b/client/settings/express-checkout-settings/index.js @@ -10,7 +10,6 @@ import { __ } from '@wordpress/i18n'; */ import './index.scss'; import SettingsSection from '../settings-section'; -import { getPaymentSettingsUrl } from '../../utils'; import PaymentRequestSettings from './payment-request-settings'; import WooPaySettings from './woopay-settings'; import SettingsLayout from '../settings-layout'; @@ -137,15 +136,10 @@ const ExpressCheckoutSettings = ( { methodId } ) => { } ); } - const { title, sections, controls: Controls } = method; + const { sections, controls: Controls } = method; return ( -

- { 'WooPayments' } >{ ' ' } - { title } -

- { sections.map( ( { section, description } ) => ( diff --git a/client/settings/express-checkout/style.scss b/client/settings/express-checkout/style.scss index f515674c74f..71bacfad562 100644 --- a/client/settings/express-checkout/style.scss +++ b/client/settings/express-checkout/style.scss @@ -155,3 +155,38 @@ } } } + +body { + &.woocommerce-payments-checkout-section { + .express-checkouts { + .express-checkouts-list { + .express-checkout { + padding-left: 0; + padding-right: 0; + &:first-child { + padding-top: 0; + } + &:last-child { + padding-bottom: 0; + } + } + .express-checkout__label-container { + display: block; + } + } + } + } +} + +@media ( min-width: 661px ) { + body { + &.woocommerce-payments-checkout-section { + .express-checkout__text-container { + width: 100%; + } + .express-checkout__link { + margin-left: auto; + } + } + } +} diff --git a/client/settings/fraud-protection/components/protection-levels/index.tsx b/client/settings/fraud-protection/components/protection-levels/index.tsx index af29e04b757..6f22c2a924b 100644 --- a/client/settings/fraud-protection/components/protection-levels/index.tsx +++ b/client/settings/fraud-protection/components/protection-levels/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { __ } from '@wordpress/i18n'; import HelpOutlineIcon from 'gridicons/dist/help-outline'; import { useState } from '@wordpress/element'; @@ -13,6 +13,8 @@ import { Button } from '@wordpress/components'; import { useCurrentProtectionLevel, useAdvancedFraudProtectionSettings, + useSettings, + useGetSettings, } from 'wcpay/data'; import { FraudProtectionHelpText, BasicFraudProtectionModal } from '../index'; import { getAdminUrl } from 'wcpay/utils'; @@ -24,6 +26,9 @@ import { CurrentProtectionLevelHook } from '../../interfaces'; const ProtectionLevels: React.FC = () => { const [ isBasicModalOpen, setBasicModalOpen ] = useState( false ); + const initialProtectionLevelRef = useRef< string | null >( null ); + const initialSettingsRef = useRef< Record< string, any > | null >( null ); + const [ currentProtectionLevel, updateProtectionLevel, @@ -33,6 +38,19 @@ const ProtectionLevels: React.FC = () => { advancedFraudProtectionSettings, ] = useAdvancedFraudProtectionSettings(); + const { isDirty } = useSettings(); + const currentSettings = useGetSettings() as Record< string, any >; + + useEffect( () => { + if ( initialProtectionLevelRef.current === null ) { + initialProtectionLevelRef.current = currentProtectionLevel; + } + + if ( initialSettingsRef.current === null && currentSettings ) { + initialSettingsRef.current = { ...currentSettings }; + } + }, [ currentProtectionLevel, currentSettings ] ); + const isAdvancedSettingsConfigured = Array.isArray( advancedFraudProtectionSettings ) && 0 < advancedFraudProtectionSettings.length; @@ -49,6 +67,62 @@ const ProtectionLevels: React.FC = () => { setBasicModalOpen( true ); }; + // Check if only the protection level setting has changed + const isOnlyProtectionLevelChanged = (): boolean => { + if ( ! initialSettingsRef.current || ! currentSettings ) { + return false; + } + + const allKeys = new Set( [ + ...Object.keys( initialSettingsRef.current ), + ...Object.keys( currentSettings ), + ] ); + + // Check each key to see if anything other than protection level changed + for ( const key of allKeys ) { + if ( key === 'current_protection_level' ) { + continue; + } + + const initialValue = + initialSettingsRef.current[ key ] !== undefined + ? initialSettingsRef.current[ key ] + : null; + const currentValue = + currentSettings[ key ] !== undefined + ? currentSettings[ key ] + : null; + + // If values are different for any key other than protection level, more than one setting changed + if ( + JSON.stringify( initialValue ) !== + JSON.stringify( currentValue ) + ) { + return false; + } + } + + // If we got here, only the protection level changed + return true; + }; + + const handleConfigureClick = () => { + // Only clear the beforeunload handler if: + // 1. The page has unsaved changes (isDirty is true) + // 2. The initial protection level was Basic + // 3. The current protection level is Advanced + // 4. It's the only setting that changed on the page + if ( + isDirty && + initialProtectionLevelRef.current === ProtectionLevel.BASIC && + currentProtectionLevel === ProtectionLevel.ADVANCED && + isOnlyProtectionLevelChanged() + ) { + // When the only change is from Basic to Advanced, prevent the dialog + window.onbeforeunload = null; + } + }; + return ( <> { 'error' === advancedFraudProtectionSettings && ( @@ -64,7 +138,10 @@ const ProtectionLevels: React.FC = () => { ) } ) } -
+
  • @@ -138,6 +215,7 @@ const ProtectionLevels: React.FC = () => { path: '/payments/fraud-protection', } ) } isSecondary + onClick={ handleConfigureClick } disabled={ ProtectionLevel.ADVANCED !== currentProtectionLevel diff --git a/client/settings/fraud-protection/components/protection-levels/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/components/protection-levels/test/__snapshots__/index.test.tsx.snap index 9fc6a497771..b3d745e1b71 100644 --- a/client/settings/fraud-protection/components/protection-levels/test/__snapshots__/index.test.tsx.snap +++ b/client/settings/fraud-protection/components/protection-levels/test/__snapshots__/index.test.tsx.snap @@ -2,7 +2,9 @@ exports[`ProtectionLevels renders 1`] = `
    -
    +
      • @@ -219,7 +222,9 @@ exports[`ProtectionLevels renders an error message when settings can not be fetc exports[`ProtectionLevels renders with the advanced fraud protection settings selected 1`] = `
        -
        +
        • {

          - - { __( - 'Set your payment risk level', - 'woocommerce-payments' - ) } - + { __( + 'Set your payment risk level', + 'woocommerce-payments' + ) }

          diff --git a/client/settings/fraud-protection/style.scss b/client/settings/fraud-protection/style.scss index 88ac2ca4292..b112290e454 100644 --- a/client/settings/fraud-protection/style.scss +++ b/client/settings/fraud-protection/style.scss @@ -301,12 +301,6 @@ } } -#fraud-protection-welcome-tour-first-step { - bottom: 26px; - left: 207px; - position: absolute; -} - -.tour-kit-frame__arrow[data-hide]::before { - visibility: hidden; +.tour-kit-frame__arrow::before { + background: #fff; } diff --git a/client/settings/fraud-protection/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/test/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 9d2e5130894..00000000000 --- a/client/settings/fraud-protection/test/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,247 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FraudProtection should render correctly 1`] = ` -
          -
          -
          -
          -

          - - Set your payment risk level - -

          -
          -
            -
          • -
            - - - - - - - -
            -

            - Provides the base level of platform protection. -

            -
          • -
            -
          • - - -
          • -
          -
          -
          -
          - @@ -4652,7 +4652,7 @@ exports[`Transactions list when not filtered by payout renders correctly 1`] = ` >
          @@ -5358,7 +5358,7 @@ exports[`Transactions list when not filtered by payout renders table summary onl >
          @@ -5539,7 +5539,7 @@ exports[`Transactions list when not filtered by payout renders table summary onl >
          @@ -5687,7 +5687,7 @@ exports[`Transactions list when not filtered by payout renders table summary onl >
        diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx index 364a4d05138..c7ea9138ea9 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -10,6 +10,7 @@ import apiFetch from '@wordpress/api-fetch'; import { getQuery, updateQueryString } from '@woocommerce/navigation'; import { useUserPreferences } from '@woocommerce/data'; import { getUserTimeZone } from 'wcpay/utils/test-utils'; +import { PAYMENT_METHOD_BRANDS } from 'wcpay/constants/payment-method'; /** * Internal dependencies @@ -75,6 +76,9 @@ const mockUseUserPreferences = useUserPreferences as jest.MockedFunction< declare const global: { wcpaySettings: { + accountStatus: { + country: string; + }; isSubscriptionsActive: boolean; featureFlags: { customSearch: boolean; @@ -98,6 +102,7 @@ declare const global: { code: string; }; }; + wooPaymentsPaymentMethodsConfig: Record< string, { title: string } >; }; const getMockTransactions: () => Transaction[] = () => [ @@ -106,7 +111,7 @@ const getMockTransactions: () => Transaction[] = () => [ transaction_id: 'txn_j23jda9JJa', date: '2020-01-02 17:46:02', type: 'refund', - source: 'visa', + source: PAYMENT_METHOD_BRANDS.VISA, order: { id: 123, number: 'custom-123', @@ -138,7 +143,7 @@ const getMockTransactions: () => Transaction[] = () => [ date: '2020-01-05 04:22:59', available_on: '2020-01-07 00:00:00', type: 'charge', - source: 'mastercard', + source: PAYMENT_METHOD_BRANDS.MASTERCARD, order: { id: 123, number: 'custom-125', @@ -170,7 +175,7 @@ const getMockTransactions: () => Transaction[] = () => [ transaction_id: 'txn_mmtr89gjh5', date: '2020-01-02 19:55:05', type: 'charge', - source: 'visa', + source: PAYMENT_METHOD_BRANDS.VISA, order: { id: 123, number: 'custom-335', @@ -213,6 +218,9 @@ describe( 'Transactions list', () => { } as any ); global.wcpaySettings = { + accountStatus: { + country: 'US', + }, featureFlags: { customSearch: true, }, @@ -236,6 +244,9 @@ describe( 'Transactions list', () => { code: 'en', }, }; + + global.wooPaymentsPaymentMethodsConfig = {}; + window.wcpaySettings.dateFormat = 'M j, Y'; window.wcpaySettings.timeFormat = 'g:iA'; } ); diff --git a/client/transactions/utils/getTransactionPaymentMethodTitle.ts b/client/transactions/utils/getTransactionPaymentMethodTitle.ts new file mode 100644 index 00000000000..f7a08aa6970 --- /dev/null +++ b/client/transactions/utils/getTransactionPaymentMethodTitle.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { TRANSACTION_PAYMENT_METHOD_TITLES } from 'wcpay/constants/payment-method'; + +type TransactionPaymentMethodId = + | keyof typeof TRANSACTION_PAYMENT_METHOD_TITLES + | string; + +/** + * Gets the display title for a payment method in the transaction context. + * + * @param {TransactionPaymentMethodId} paymentMethodId - The ID of the payment method or transaction type + * @param {Record} [config] - Optional payment method configuration. Defaults to window.wooPaymentsPaymentMethodsConfig + * @return {string} The display title for the payment method + */ +export const getTransactionPaymentMethodTitle = ( + paymentMethodId: TransactionPaymentMethodId, + config = window?.wooPaymentsPaymentMethodsConfig +): string => { + return ( + config?.[ paymentMethodId ]?.title || + TRANSACTION_PAYMENT_METHOD_TITLES[ + paymentMethodId as keyof typeof TRANSACTION_PAYMENT_METHOD_TITLES + ] || + paymentMethodId + ); +}; diff --git a/client/transactions/utils/test/getTransactionPaymentMethodTitle.test.ts b/client/transactions/utils/test/getTransactionPaymentMethodTitle.test.ts new file mode 100644 index 00000000000..6047faeb5e2 --- /dev/null +++ b/client/transactions/utils/test/getTransactionPaymentMethodTitle.test.ts @@ -0,0 +1,74 @@ +/** + * Internal dependencies + */ +import { getTransactionPaymentMethodTitle } from '../getTransactionPaymentMethodTitle'; +import { TRANSACTION_PAYMENT_METHOD_TITLES } from 'wcpay/constants/payment-method'; + +describe( 'getTransactionPaymentMethodTitle', () => { + const mockConfig = { + custom_method: { + isReusable: true, + isBnpl: false, + title: 'Custom Payment Method', + icon: 'custom_icon', + darkIcon: 'custom_dark_icon', + showSaveOption: true, + countries: [ 'US', 'CA' ], + testingInstructions: 'Custom testing instructions', + forceNetworkSavedCards: false, + }, + visa: { + isReusable: true, + isBnpl: false, + title: 'Custom Visa Title', + icon: 'visa_icon', + darkIcon: 'visa_dark_icon', + showSaveOption: true, + countries: [ 'US', 'CA' ], + testingInstructions: 'Custom testing instructions', + forceNetworkSavedCards: false, + }, + }; + + beforeEach( () => { + // Clear any global config before each test + delete window.wooPaymentsPaymentMethodsConfig; + } ); + + it( 'should return title from provided config when available', () => { + expect( + getTransactionPaymentMethodTitle( 'custom_method', mockConfig ) + ).toBe( 'Custom Payment Method' ); + } ); + + it( 'should return title from global config when available', () => { + window.wooPaymentsPaymentMethodsConfig = mockConfig; + expect( getTransactionPaymentMethodTitle( 'custom_method' ) ).toBe( + 'Custom Payment Method' + ); + } ); + + it( 'should fallback to TRANSACTION_PAYMENT_METHOD_TITLES when method not in config', () => { + expect( getTransactionPaymentMethodTitle( 'visa', {} ) ).toBe( + TRANSACTION_PAYMENT_METHOD_TITLES.visa + ); + } ); + + it( 'should prefer config title over TRANSACTION_PAYMENT_METHOD_TITLES', () => { + expect( getTransactionPaymentMethodTitle( 'visa', mockConfig ) ).toBe( + 'Custom Visa Title' + ); + } ); + + it( 'should fallback to payment method ID when no title is found', () => { + expect( getTransactionPaymentMethodTitle( 'unknown_method', {} ) ).toBe( + 'unknown_method' + ); + } ); + + it( 'should handle undefined config gracefully', () => { + expect( getTransactionPaymentMethodTitle( 'visa', undefined ) ).toBe( + TRANSACTION_PAYMENT_METHOD_TITLES.visa + ); + } ); +} ); diff --git a/client/types/charges.d.ts b/client/types/charges.d.ts index 011ee1f1c0b..0d70088c473 100644 --- a/client/types/charges.d.ts +++ b/client/types/charges.d.ts @@ -3,6 +3,7 @@ */ import { BalanceTransaction } from './balance-transactions'; import { Dispute } from './disputes'; +import PAYMENT_METHOD_IDS from 'wcpay/constants/payment-method'; interface ChargeBillingDetails { email: null | string; @@ -27,25 +28,11 @@ interface ChargeRefunds { data: ChargeRefund[]; } -export interface PaymentMethodDetails { - card?: any; - grabpay?: any; - type: - | 'affirm' - | 'afterpay_clearpay' - | 'au_becs_debit' - | 'bancontact' - | 'card' - | 'card_present' - | 'eps' - | 'giropay' - | 'ideal' - | 'klarna' - | 'grabpay' - | 'p24' - | 'sepa_debit' - | 'sofort'; -} +export type PaymentMethodDetails = { + [ T in PAYMENT_METHOD_IDS ]: { + type: T; + } & Record< T, Record< string, unknown > >; +}[ PAYMENT_METHOD_IDS ]; export type OutcomeRiskLevel = | 'normal' diff --git a/client/types/payment-methods.d.ts b/client/types/payment-methods.d.ts index 350659ee2f5..7961d006203 100644 --- a/client/types/payment-methods.d.ts +++ b/client/types/payment-methods.d.ts @@ -6,30 +6,18 @@ * Internal dependencies */ -export type PaymentMethod = - | 'affirm' - | 'afterpay_clearpay' - | 'au_becs_debit' - | 'bancontact' - | 'card' - | 'card_present' - | 'eps' - | 'klarna' - | 'grabpay' - | 'giropay' - | 'ideal' - | 'p24' - | 'sepa_debit' - | 'sofort'; - -export interface PaymentMethodMapEntry { +export interface PaymentMethodServerDefinition { id: string; label: string; description: string; - icon: ReactImgFuncComponent; + settings_icon_url: string; currencies: string[]; stripe_key: string; allows_manual_capture: boolean; allows_pay_later: boolean; accepts_only_domestic_payment: boolean; } + +export interface PaymentMethodMapEntry extends PaymentMethodServerDefinition { + icon: ReactImgFuncComponent; +} diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 7346b408e1f..ece041912c7 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -14,9 +14,9 @@ import { formatCurrency } from 'multi-currency/interface/functions'; import { formatFee } from 'utils/fees'; import React from 'react'; import { BaseFee, DiscountFee, FeeStructure } from 'wcpay/types/fees'; -import { PaymentMethod } from 'wcpay/types/payment-methods'; import { createInterpolateElement } from '@wordpress/element'; import { ExternalLink } from '@wordpress/components'; +import PAYMENT_METHOD_IDS from 'constants/payment-method'; const countryFeeStripeDocsBaseLink = 'https://woocommerce.com/document/woopayments/fees-and-debits/fees/#'; @@ -350,47 +350,27 @@ export const formatMethodFeesDescription = ( }; export const getTransactionsPaymentMethodName = ( - paymentMethod: PaymentMethod + paymentMethod: PAYMENT_METHOD_IDS ): string => { + // Special cases that won't be in wooPaymentsPaymentMethodsConfig + // `card` WILL be in that config, but it's title is "Cards" and we want to show "Card transactions." switch ( paymentMethod ) { - case 'au_becs_debit': - return __( - 'BECS Direct Debit transactions', - 'woocommerce-payments' - ); - case 'bancontact': - return __( 'Bancontact transactions', 'woocommerce-payments' ); case 'card': return __( 'Card transactions', 'woocommerce-payments' ); case 'card_present': return __( 'In-person transactions', 'woocommerce-payments' ); - case 'eps': - return __( 'EPS transactions', 'woocommerce-payments' ); - case 'giropay': - return __( 'giropay transactions', 'woocommerce-payments' ); - case 'ideal': - return __( 'iDEAL transactions', 'woocommerce-payments' ); - case 'p24': - return __( - 'Przelewy24 (P24) transactions', - 'woocommerce-payments' - ); - case 'sepa_debit': - return __( - 'SEPA Direct Debit transactions', - 'woocommerce-payments' - ); - case 'sofort': - return __( 'Sofort transactions', 'woocommerce-payments' ); - case 'affirm': - return __( 'Affirm transactions', 'woocommerce-payments' ); - case 'afterpay_clearpay': - return __( 'Afterpay transactions', 'woocommerce-payments' ); - case 'klarna': - return __( 'Klarna transactions', 'woocommerce-payments' ); - case 'grabpay': - return __( 'GrabPay transactions', 'woocommerce-payments' ); - default: - return __( 'Unknown transactions', 'woocommerce-payments' ); } + + // Try to get the title from wooPaymentsPaymentMethodsConfig + const methodConfig = wooPaymentsPaymentMethodsConfig[ paymentMethod ]; + if ( methodConfig?.title ) { + return sprintf( + /* translators: %s: Payment method title */ + __( '%s transactions', 'woocommerce-payments' ), + methodConfig.title + ); + } + + // Fallback for unknown payment methods + return __( 'Unknown transactions', 'woocommerce-payments' ); }; diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 02643e043ff..e3509a31a8a 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -11,6 +11,7 @@ use WCPay\Core\Server\Request; use WCPay\Database_Cache; use WCPay\Inline_Script_Payloads\Woo_Payments_Payment_Method_Definitions; +use WCPay\Inline_Script_Payloads\Woo_Payments_Payment_Methods_Config; use WCPay\Logger; use WCPay\WooPay\WooPay_Utilities; @@ -171,6 +172,7 @@ public function init_hooks() { add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'display_wcpay_transaction_fee' ] ); add_action( 'admin_init', [ $this, 'redirect_deposits_to_payouts' ] ); add_action( 'woocommerce_update_options_site-visibility', [ $this, 'inform_stripe_when_store_goes_live' ] ); + add_action( 'admin_init', [ $this, 'add_css_classes' ] ); } /** @@ -605,6 +607,12 @@ public function register_payments_scripts() { 'before' ); + wp_add_inline_script( + 'WCPAY_DASH_APP', + new Woo_Payments_Payment_Methods_Config(), + 'before' + ); + wp_set_script_translations( 'WCPAY_DASH_APP', 'woocommerce-payments' ); WC_Payments_Utils::register_style( @@ -992,7 +1000,7 @@ private function get_js_settings(): array { 'showUpdateDetailsTask' => $this->get_should_show_update_business_details_task( $account_status_data ), 'wpcomReconnectUrl' => $this->payments_api_client->is_server_connected() && ! $this->payments_api_client->has_server_connection_owner() ? WC_Payments_Account::get_wpcom_reconnect_url() : null, 'multiCurrencySetup' => [ - 'isSetupCompleted' => get_option( 'wcpay_multi_currency_setup_completed' ), + 'isSetupCompleted' => filter_var( get_option( 'wcpay_multi_currency_setup_completed' ), FILTER_VALIDATE_BOOLEAN ) ? 'yes' : 'no,', ], 'isMultiCurrencyEnabled' => WC_Payments_Features::is_customer_multi_currency_enabled(), 'shouldUseExplicitPrice' => WC_Payments_Explicit_Price_Formatter::should_output_explicit_price(), @@ -1012,7 +1020,6 @@ private function get_js_settings(): array { 'enabledPaymentMethods' => $this->get_enabled_payment_method_ids(), 'progressiveOnboarding' => $this->account->get_progressive_onboarding_details(), 'accountDefaultCurrency' => $this->account->get_account_default_currency(), - 'frtDiscoverBannerSettings' => get_option( 'wcpay_frt_discover_banner_settings', '' ), 'storeCurrency' => get_option( 'woocommerce_currency' ), 'isWooPayStoreCountryAvailable' => WooPay_Utilities::is_store_country_available(), 'woopayLastDisableDate' => $this->wcpay_gateway->get_option( 'platform_checkout_last_disable_date' ), @@ -1294,7 +1301,7 @@ public function show_woopay_payment_method_name_admin( $order_id ) { ?>
        - WooPay + WooPay get_meta( 'last4' ) ) { echo esc_html_e( 'Card ending in', 'woocommerce-payments' ) . ' '; @@ -1394,6 +1401,24 @@ public function add_transactions_notification_badge() { } } + /** + * Add new custom css classes. + * + * @return void + */ + public function add_css_classes() { + global $current_tab; + + if ( 'checkout' === $current_tab ) { + add_filter( + 'admin_body_class', + static function ( $classes ) { + return "$classes woocommerce-payments-checkout-section"; + } + ); + } + } + /** * Gets the number of disputes which need a response. ie have a 'needs_response' or 'warning_needs_response' status. * Used to display a notification badge on the Payments > Disputes menu item. diff --git a/includes/admin/class-wc-rest-payments-onboarding-controller.php b/includes/admin/class-wc-rest-payments-onboarding-controller.php index 3903dad3ff7..9d5fd2138da 100644 --- a/includes/admin/class-wc-rest-payments-onboarding-controller.php +++ b/includes/admin/class-wc-rest-payments-onboarding-controller.php @@ -59,7 +59,9 @@ public function register_routes() { 'progressive' => [ 'required' => false, 'description' => 'Whether the session is for progressive onboarding.', - 'type' => 'string', + // phpcs:ignore Squiz.PHP.CommentedOutCode.Found + // We expect a boolean (true, false, 0, 1, '0', '1', 'true', or 'false'), but will also accept `yes`/`no`. + 'type' => [ 'boolean', 'string' ], ], 'self_assessment' => [ 'required' => false, @@ -91,9 +93,9 @@ public function register_routes() { 'description' => 'The timeframe bucket for the estimated first live transaction.', 'required' => true, ], - 'url' => [ + 'site' => [ 'type' => 'string', - 'description' => 'The URL of the store.', + 'description' => 'The URL of the site.', 'required' => true, ], ], @@ -124,6 +126,16 @@ public function register_routes() { ] ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/fields', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_fields' ], + 'permission_callback' => [ $this, 'check_permission' ], + ] + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/business_types', @@ -189,6 +201,24 @@ public function register_routes() { 'permission_callback' => [ $this, 'check_permission' ], ] ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/test_drive_account/init', + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'init_test_drive_account' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'capabilities' => [ + 'required' => false, + 'description' => 'The capabilities to request and enable for the test-drive account. Leave empty to use the default capabilities.', + 'type' => 'array', + 'default' => [], + ], + ], + ] + ); } /** @@ -200,7 +230,7 @@ public function register_routes() { */ public function get_embedded_kyc_session( WP_REST_Request $request ) { $self_assessment_data = ! empty( $request->get_param( 'self_assessment' ) ) ? wc_clean( wp_unslash( $request->get_param( 'self_assessment' ) ) ) : []; - $progressive = ! empty( $request->get_param( 'progressive' ) ) && 'true' === $request->get_param( 'progressive' ); + $progressive = ! empty( $request->get_param( 'progressive' ) ) && filter_var( $request->get_param( 'progressive' ), FILTER_VALIDATE_BOOLEAN ); $account_session = $this->onboarding_service->create_embedded_kyc_session( $self_assessment_data, @@ -256,6 +286,22 @@ public function finalize_embedded_kyc( WP_REST_Request $request ) { ); } + /** + * Get fields data via API. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error + */ + public function get_fields( WP_REST_Request $request ) { + $fields = $this->onboarding_service->get_fields_data( get_user_locale() ); + if ( is_null( $fields ) ) { + return new WP_Error( self::RESULT_BAD_REQUEST, 'Failed to retrieve the onboarding fields.', [ 'status' => 400 ] ); + } + + return rest_ensure_response( [ 'data' => $fields ] ); + } + /** * Get business types via API. * @@ -285,4 +331,25 @@ public function get_progressive_onboarding_eligible( WP_REST_Request $request ) ] ); } + + /** + * Initialize a test-drive account. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error + */ + public function init_test_drive_account( WP_REST_Request $request ) { + try { + $success = $this->onboarding_service->init_test_drive_account( $request->get_param( 'capabilities' ) ); + } catch ( Exception $e ) { + return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] ); + } + + return rest_ensure_response( + [ + 'success' => $success, + ] + ); + } } diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index d6c85027359..7a60f99c1d9 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -456,7 +456,7 @@ public function create_terminal_intent( $request ) { * @param WP_REST_Request $request Request object. * @param array $default_value - default value. * - * @return array|null + * @return array * @throws \Exception */ public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ): array { @@ -484,7 +484,7 @@ public function get_terminal_intent_payment_method( $request, array $default_val * @param WP_REST_Request $request Request object. * @param string $default_value default value. * - * @return string|null + * @return string * @throws \Exception */ public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ): string { diff --git a/includes/admin/class-wc-rest-payments-settings-option-controller.php b/includes/admin/class-wc-rest-payments-settings-option-controller.php new file mode 100644 index 00000000000..28e0c8f8c8a --- /dev/null +++ b/includes/admin/class-wc-rest-payments-settings-option-controller.php @@ -0,0 +1,111 @@ +namespace, + '/' . $this->rest_base . '/(?P[a-zA-Z0-9_-]+)', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'update_option' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'option_name' => [ + 'required' => true, + 'validate_callback' => [ $this, 'validate_option_name' ], + ], + 'value' => [ + 'required' => true, + 'validate_callback' => [ $this, 'validate_value' ], + ], + ], + ] + ); + } + + /** + * Validate the option name. + * + * @param string $option_name The option name to validate. + * @return bool + */ + public function validate_option_name( string $option_name ): bool { + return in_array( $option_name, self::ALLOWED_OPTIONS, true ); + } + + /** + * Validate the value parameter. + * + * @param mixed $value The value to validate. + * @return bool|WP_Error True if valid, WP_Error if invalid. + */ + public function validate_value( $value ) { + if ( is_bool( $value ) || is_array( $value ) ) { + return true; + } + return new WP_Error( + 'rest_invalid_param', + __( 'Invalid value type; must be either boolean or array', 'woocommerce-payments' ), + [ 'status' => 400 ] + ); + } + + /** + * Update the option value. + * + * @param WP_REST_Request $request The request object. + * @return WP_Error|WP_REST_Response + */ + public function update_option( WP_REST_Request $request ) { + $option_name = $request->get_param( 'option_name' ); + $value = $request->get_param( 'value' ); + + update_option( $option_name, $value ); + + return rest_ensure_response( + [ + 'success' => true, + ] + ); + } +} diff --git a/includes/class-duplicates-detection-service.php b/includes/class-duplicates-detection-service.php index afa8124f3d1..446275aad5a 100644 --- a/includes/class-duplicates-detection-service.php +++ b/includes/class-duplicates-detection-service.php @@ -94,8 +94,10 @@ private function search_for_cc() { * @return Duplicates_Detection_Service */ private function search_for_additional_payment_methods() { - // Get all payment method definitions. - + /** + * FLAG: PAYMENT_METHODS_LIST + * As payment methods are converted to use definitions, they need to be removed from the list below. + */ $keywords = [ 'bancontact' => Bancontact_Payment_Method::PAYMENT_METHOD_STRIPE_ID, 'sepa' => Sepa_Payment_Method::PAYMENT_METHOD_STRIPE_ID, @@ -112,6 +114,7 @@ private function search_for_additional_payment_methods() { 'wechatpay' => Wechatpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, ]; + // Get all payment method definitions. $payment_method_definitions = PaymentMethodDefinitionRegistry::instance()->get_all_payment_method_definitions(); // This gets all the registered payment method definitions. As new payment methods are converted from the legacy style, they need to be removed from the list above. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 761dcae591a..fad701de8f7 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -20,6 +20,7 @@ use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Type; use WCPay\Constants\Payment_Method; +use WCPay\Constants\Refund_Status; use WCPay\Exceptions\{Add_Payment_Method_Exception, Amount_Too_Small_Exception, API_Merchant_Exception, @@ -330,6 +331,12 @@ public function __construct( $this->id = self::GATEWAY_ID . '_' . $this->stripe_id; } + /** + * FLAG: PAYMENT_METHODS_LIST + * Once all payment methods are converted to use definitions, they will all + * have a get_stripe_id() method that can be used instead of this map. + */ + // 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. @@ -375,8 +382,10 @@ public function __construct( * @return string */ public function get_title() { - $this->title = $this->payment_method->get_title(); - $this->method_title = "WooPayments ($this->title)"; + if ( ! $this->title ) { + $this->title = $this->payment_method->get_title(); + $this->method_title = "WooPayments ($this->title)"; + } return parent::get_title(); } @@ -973,6 +982,25 @@ public function output_payments_settings_screen() { global $hide_save_button; $hide_save_button = true; + $method_title = $this->get_method_title(); + $return_url = 'admin.php?page=wc-settings&tab=checkout'; + if ( ! empty( $_GET['method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + // Override the title and return url for method-specific pages in WooPayments settings. + $method = sanitize_text_field( wp_unslash( $_GET['method'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $method_title = 'payment_request' === $method ? 'Apple Pay / Google Pay' : ( 'woopay' === $method ? 'WooPay' : $this->get_method_title() ); + $return_url = 'admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments'; + } + + if ( function_exists( 'wc_back_header' ) ) { + wc_back_header( $method_title, __( 'Return to payments', 'woocommerce-payments' ), $return_url ); + } else { + // Until the wc_back_header function is available (WC Core 9.9) use the current available version. + echo '

        '; + echo esc_html( $method_title ); + wc_back_link( __( 'Return to payments', 'woocommerce-payments' ), $return_url ); + echo '

        '; + } + if ( ! empty( $_GET['method'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
        order_service->mark_payment_failed( $order, $intent_id, $status, $charge_id ); } } else { - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $woopay_intent_id = sanitize_user( wp_unslash( $_POST['platform-checkout-intent'] ?? '' ), true ); + // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $woopay_intent_id = WooPay_Utilities::sanitize_intent_id( wp_unslash( $_POST['platform-checkout-intent'] ?? '' ) ); if ( ! empty( $woopay_intent_id ) ) { // If the setup intent is included in the request use that intent. @@ -2310,8 +2336,8 @@ static function ( $refund ) use ( $refund_amount ) { // translators: %1$: order id. return new WP_Error( 'wcpay_edit_order_refund_not_found', sprintf( __( 'A refund cannot be found for order: %1$s', 'woocommerce-payments' ), $order->get_id() ) ); } - // If the refund was successful, add a note to the order and update the refund status. - $this->order_service->add_note_and_metadata_for_refund( $order, $wc_refund, $refund['id'], $refund['balance_transaction'] ?? null ); + // There is no error. Refund status can be either pending or succeeded, add a note to the order and update the refund status. + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund['id'], $refund['balance_transaction'] ?? null, Refund_Status::PENDING === $refund['status'] ); return true; } @@ -2323,7 +2349,7 @@ static function ( $refund ) use ( $refund_amount ) { * @return boolean */ public function has_refund_failed( $order ) { - return 'failed' === $this->order_service->get_wcpay_refund_status_for_order( $order ); + return Refund_Status::FAILED === $this->order_service->get_wcpay_refund_status_for_order( $order ); } /** @@ -4039,6 +4065,10 @@ public function get_payment_method_ids_enabled_at_checkout_filtered_by_fees( $or public function get_upe_available_payment_methods() { $available_methods = [ 'card' ]; + /** + * FLAG: PAYMENT_METHODS_LIST + * As payment methods are converted to use definitions, they need to be removed from the list below. + */ $available_methods[] = Becs_Payment_Method::PAYMENT_METHOD_STRIPE_ID; $available_methods[] = Bancontact_Payment_Method::PAYMENT_METHOD_STRIPE_ID; $available_methods[] = Eps_Payment_Method::PAYMENT_METHOD_STRIPE_ID; diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 8272d979bb5..8f76371caa6 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -40,6 +40,7 @@ class WC_Payments_Account implements MultiCurrencyAccountInterface { const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE = 'wcpay_account_connect_wpcom_connection_failure'; const TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED = 'wcpay_account_connect_finished'; const TRACKS_EVENT_KYC_REMINDER_MERCHANT_RETURNED = 'wcpay_kyc_reminder_merchant_returned'; + const TRACKS_EVENT_ACCOUNT_REFERRAL = 'wcpay_account_referral'; /** * Client for making requests to the WooCommerce Payments API @@ -113,6 +114,7 @@ public function init_hooks() { add_action( 'admin_init', [ $this, 'maybe_redirect_after_plugin_activation' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. add_action( 'admin_init', [ $this, 'maybe_redirect_by_get_param' ], 12 ); // Run this after the redirect to onboarding logic. // Third, handle page redirections. + add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_referral' ], 13 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_settings_page' ], 15 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_onboarding_wizard_page' ], 15 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_connect_page' ], 15 ); @@ -868,6 +870,48 @@ public function maybe_redirect_after_plugin_activation(): bool { return true; } + /** + * Stores the account referral code and redirects to the connect page. + * + * @return void + */ + public function maybe_redirect_onboarding_referral(): void { + if ( ! is_admin() || wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + if ( ! isset( $_GET['woopayments-ref'] ) ) { + return; + } + + // Return early and redirect to the overview page if already connected. + if ( $this->is_stripe_account_valid() ) { + $this->redirect_service->redirect_to_overview_page(); + return; + } + + $referral_code = sanitize_text_field( wp_unslash( $_GET['woopayments-ref'] ) ); + $referral_code = $this->onboarding_service->normalize_and_store_referral_code( $referral_code ); + + // Return and redirect early if the code is invalid. + if ( empty( $referral_code ) ) { + $this->redirect_service->redirect_to_connect_page(); + return; + } + + // Track the referral code. + $this->tracks_event( + self::TRACKS_EVENT_ACCOUNT_REFERRAL, + [ + 'referral_code' => $referral_code, + 'referrer' => wp_get_referer(), + ] + ); + + // Redirect to the connect page. + $this->redirect_service->redirect_to_connect_page( null, WC_Payments_Onboarding_Service::FROM_REFERRAL ); + } + /** * Redirects WooPayments settings to the connect page when there is no account or an invalid account. * @@ -1035,6 +1079,7 @@ public function maybe_redirect_from_connect_page(): bool { WC_Payments_Onboarding_Service::get_from(), [ WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD, + WC_Payments_Onboarding_Service::FROM_WCADMIN_NOX_IN_CONTEXT, WC_Payments_Onboarding_Service::FROM_ONBOARDING_KYC, ], true @@ -1052,19 +1097,27 @@ public function maybe_redirect_from_connect_page(): bool { // Determine from where the merchant was directed to the Connect page. $from = WC_Payments_Onboarding_Service::get_from(); - // If the user came from the core Payments task list item, + // If the user came from the core Payments task list item or the WC Payments Settings NOX in-context flow, // skip the Connect page and go directly to the Jetpack connection flow and/or onboarding wizard. - if ( WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK === $from ) { + if ( in_array( + $from, + [ + WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK, + WC_Payments_Onboarding_Service::FROM_WCADMIN_NOX_IN_CONTEXT, + ], + true + ) ) { // We use a connect link to allow our logic to determine what comes next: // the Jetpack connection setup and/or onboarding wizard (MOX). $this->redirect_service->redirect_to_wcpay_connect( - // The next step should treat the merchant as coming from the Payments task list item, + // The next step should treat the merchant as coming from the originating place, // not the Connect page. - WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK, + $from, [ 'source' => WC_Payments_Onboarding_Service::get_source(), ] ); + return true; } @@ -1929,6 +1982,14 @@ private function get_onboarding_return_url( string $wcpay_connect_from ): string switch ( $wcpay_connect_from ) { case 'WC_SUBSCRIPTIONS_TABLE': return admin_url( add_query_arg( [ 'post_type' => 'shop_subscription' ], 'edit.php' ) ); + case WC_Payments_Onboarding_Service::FROM_WCADMIN_NOX_IN_CONTEXT: + // Build the URL to point to the WC NOX in-context onboarding. + $params = [ + 'page' => 'wc-admin', + 'tab' => 'checkout', + 'path' => '/woopayments/onboarding', + ]; + return admin_url( add_query_arg( $params, 'admin.php' ) ); default: return static::get_connect_url(); } @@ -2034,10 +2095,16 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. WC_Payments_Onboarding_Service::get_actioned_notes(), $progressive, - $collect_payout_requirements + $collect_payout_requirements, + $this->onboarding_service->get_referral_code() + ); + + // Check if we should enable WooPay by default respecing the WooPay value from capabilities request list. + $should_enable_woopay = $this->onboarding_service->should_enable_woopay( + filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ), + $this->onboarding_service->get_capabilities_from_request() ); - $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']; @@ -2605,7 +2672,6 @@ function (): array { ); } - /** * Send a Tracks event. * diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 3ca93c093cd..47932edb00d 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -319,35 +319,67 @@ public function get_enabled_payment_method_config() { } } - $payment_method = $this->gateway->wc_payments_get_payment_method_by_id( $payment_method_id ); - $account_country = $this->account->get_account_country(); - $settings[ $payment_method_id ] = [ - 'isReusable' => $payment_method->is_reusable(), - 'title' => $payment_method->get_title( $account_country ), - 'icon' => $payment_method->get_icon( $account_country ), - 'darkIcon' => $payment_method->get_dark_icon( $account_country ), - 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ), - 'countries' => $payment_method->get_countries(), - ]; - - $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); - $settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html( - /* translators: link to Stripe testing page */ - $payment_method->get_testing_instructions( $account_country ), - [ - 'a' => '', - 'strong' => '', - 'number' => ' +
        @@ -246,7 +253,7 @@ public function show_woopay_payment_method_name( $order ) { ob_start(); ?>
        - WooPay + WooPay get_meta( 'last4' ) ) { echo esc_html_e( 'Card ending in', 'woocommerce-payments' ) . ' '; @@ -510,18 +517,18 @@ public function add_multibanco_payment_instructions_to_order_on_hold_email( $ord if ( $plain_text ) { echo "----------------------------------------\n"; - echo __( 'Multibanco Payment instructions', 'woocommerce-payments' ) . "\n\n"; + echo esc_html__( 'Multibanco Payment instructions', 'woocommerce-payments' ) . "\n\n"; printf( /* translators: %s: expiry date */ - __( 'Expires %s', 'woocommerce-payments' ) . "\n\n", - $expiry_date + esc_html__( 'Expires %s', 'woocommerce-payments' ) . "\n\n", + esc_html( $expiry_date ) ); - echo '1. ' . __( 'In your online bank account or from an ATM, choose "Payment and other services".', 'woocommerce-payments' ) . "\n"; - echo '2. ' . __( 'Click "Payments of services/shopping".', 'woocommerce-payments' ) . "\n"; - echo '3. ' . __( 'Enter the entity number, reference number, and amount.', 'woocommerce-payments' ) . "\n\n"; - echo __( 'Entity', 'woocommerce-payments' ) . ': ' . $multibanco_info['entity'] . "\n"; - echo __( 'Reference', 'woocommerce-payments' ) . ': ' . $multibanco_info['reference'] . "\n"; - echo __( 'Amount', 'woocommerce-payments' ) . ': ' . wp_strip_all_tags( $formatted_order_total ) . "\n"; + echo '1. ' . esc_html__( 'In your online bank account or from an ATM, choose "Payment and other services".', 'woocommerce-payments' ) . "\n"; + echo '2. ' . esc_html__( 'Click "Payments of services/shopping".', 'woocommerce-payments' ) . "\n"; + echo '3. ' . esc_html__( 'Enter the entity number, reference number, and amount.', 'woocommerce-payments' ) . "\n\n"; + echo esc_html__( 'Entity', 'woocommerce-payments' ) . ': ' . esc_html( $multibanco_info['entity'] ) . "\n"; + echo esc_html__( 'Reference', 'woocommerce-payments' ) . ': ' . esc_html( $multibanco_info['reference'] ) . "\n"; + echo esc_html__( 'Amount', 'woocommerce-payments' ) . ': ' . esc_html( wp_strip_all_tags( $formatted_order_total ) ) . "\n"; echo "----------------------------------------\n\n"; } else { ?> @@ -542,7 +549,7 @@ public function add_multibanco_payment_instructions_to_order_on_hold_email( $ord get_order_number() ); + echo esc_html( sprintf( __( 'Order #%s', 'woocommerce-payments' ), $order->get_order_number() ) ); ?> @@ -550,14 +557,14 @@ public function add_multibanco_payment_instructions_to_order_on_hold_email( $ord %s', 'woocommerce-payments' ), [ 'strong' => '', ] ), - $expiry_date + esc_html( $expiry_date ) ); ?> @@ -584,7 +591,7 @@ public function add_multibanco_payment_instructions_to_order_on_hold_email( $ord - + diff --git a/includes/class-wc-payments-payment-request-session.php b/includes/class-wc-payments-payment-request-session.php index 79582261e6d..78590cf9676 100644 --- a/includes/class-wc-payments-payment-request-session.php +++ b/includes/class-wc-payments-payment-request-session.php @@ -37,7 +37,6 @@ class WC_Payments_Payment_Request_Session { public function init() { // adding this filter with a higher priority than the session handler of the Store API. add_filter( 'woocommerce_session_handler', [ $this, 'add_payment_request_store_api_session_handler' ], 20 ); - add_filter( 'rest_post_dispatch', [ $this, 'store_api_headers' ], 10, 3 ); // checking to ensure we're not erasing the cart on the "order received" page. if ( $this->is_custom_session_order_received_page() ) { @@ -124,6 +123,19 @@ protected function get_session_token() { ); } + /** + * Adding the session key to the Store API response, to ensure the session can be retrieved later. + * + * @param mixed $response Response to replace the requested version with. + * + * @return mixed + */ + public function store_api_headers( $response ) { + $response->header( 'X-WooPayments-Tokenized-Cart-Session', $this->get_session_token() ); + + return $response; + } + /** * Adding the session key to the Store API response, to ensure the session can be retrieved later. * @@ -133,18 +145,17 @@ protected function get_session_token() { * * @return mixed */ - public function store_api_headers( $response, $server, $request ) { - if ( ! \WC_Payments_Utils::is_store_api_request() ) { - return $response; - } - - $nonce = $request->get_header( 'X-WooPayments-Tokenized-Cart-Session-Nonce' ); - if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_session_nonce' ) ) { - return $response; + public function maybe_clear_cart_data( $response, $server, $request ) { + if ( $request->get_header( 'X-WooPayments-Tokenized-Cart-Is-Ephemeral-Cart' ) === '1' ) { + // the customer id value doesn't matter. + // in this case, we'll be using the `WC_Payments_Payment_Request_Session_Handler` session handler, + // which will use the correct customer ID to delete. + // I am specifically calling `delete_session` instead of `forget_session` or `destroy_session`, + // because those methods might delete the customer's cookies (which we want to keep). + WC()->session->delete_session( 0 ); + WC()->cart->empty_cart(); } - $response->header( 'X-WooPayments-Tokenized-Cart-Session', $this->get_session_token() ); - return $response; } @@ -183,6 +194,10 @@ public function add_payment_request_store_api_session_handler( $default_session_ add_filter( 'woocommerce_persistent_cart_enabled', '__return_false' ); // when an order is placed via the Store API on product pages, we need to slightly modify the "order received" URL. add_filter( 'woocommerce_get_return_url', [ $this, 'store_api_order_received_return_url' ] ); + // ensuring that the `X-WooPayments-Tokenized-Cart-Session` response header is added to the response. + add_filter( 'rest_post_dispatch', [ $this, 'store_api_headers' ] ); + // clearing the cart contents if the request is made just to fetch product attributes and prices. + add_filter( 'rest_post_dispatch', [ $this, 'maybe_clear_cart_data' ], 10, 3 ); require_once WCPAY_ABSPATH . '/includes/class-wc-payments-payment-request-session-handler.php'; diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index dd38b8d2c09..c84ebdd6742 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -718,6 +718,9 @@ public static function get_filtered_error_status_code( Exception $e ): int { /** * Get the BNPL limits per currency for a specific payment method. * + * FLAG: PAYMENT_METHODS_LIST + * This can be replaced once all BNPL methods are converted to use definitions. + * * @param string $payment_method The payment method name ('affirm', 'afterpay_clearpay', or 'klarna'). * @return array The BNPL limits per currency for the specified payment method. */ @@ -1350,15 +1353,14 @@ public static function is_cart_page(): bool { } /** - * Block based themes display the cart block even when the cart shortcode is used. has_block() isn't effective - * in this case because it checks the page content for the block, which isn't present. + * Determine if the current page is a cart block. * - * @return bool + * @return bool True if the current page is a cart block, false otherwise. * * @psalm-suppress UndefinedFunction */ public static function is_cart_block(): bool { - return has_block( 'woocommerce/cart' ) || ( wp_is_block_theme() && is_cart() ); + return has_block( 'woocommerce/cart' ); } /** diff --git a/includes/class-wc-payments-webhook-processing-service.php b/includes/class-wc-payments-webhook-processing-service.php index 4a4268b9cfc..3ce4258b21e 100644 --- a/includes/class-wc-payments-webhook-processing-service.php +++ b/includes/class-wc-payments-webhook-processing-service.php @@ -14,6 +14,7 @@ use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Exceptions\Rest_Request_Exception; use WCPay\Logger; +use WCPay\Constants\Refund_Status; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -248,17 +249,15 @@ private function process_webhook_refund_updated( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); - // First, check the reason for the update. We're only interested in a status of failed. - $status = $this->read_webhook_property( $event_object, 'status' ); - if ( 'failed' !== $status ) { - return; - } - // Fetch the details of the failed refund so that we can find the associated order and write a note. - $charge_id = $this->read_webhook_property( $event_object, 'charge' ); - $refund_id = $this->read_webhook_property( $event_object, 'id' ); - $amount = $this->read_webhook_property( $event_object, 'amount' ); - $currency = $this->read_webhook_property( $event_object, 'currency' ); + $charge_id = $this->read_webhook_property( $event_object, 'charge' ); + $refund_id = $this->read_webhook_property( $event_object, 'id' ); + $amount = $this->read_webhook_property( $event_object, 'amount' ); + $currency = $this->read_webhook_property( $event_object, 'currency' ); + $status = $this->read_webhook_property( $event_object, 'status' ); + $balance_transaction = $this->has_webhook_property( $event_object, 'balance_transaction' ) + ? $this->read_webhook_property( $event_object, 'balance_transaction' ) + : null; // Look up the order related to this charge. $order = $this->wcpay_db->order_from_charge_id( $charge_id ); @@ -273,29 +272,9 @@ private function process_webhook_refund_updated( $event_body ) { ); } - $note = sprintf( - WC_Payments_Utils::esc_interpolated_html( - /* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */ - __( 'A refund of %1$s was unsuccessful using %2$s (%3$s).', 'woocommerce-payments' ), - [ - 'strong' => '', - 'code' => '', - ] - ), - WC_Payments_Explicit_Price_Formatter::get_explicit_price( - wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ), - $order - ), - 'WooPayments', - $refund_id - ); - - if ( $this->order_service->order_note_exists( $order, $note ) ) { - return; - } - + $matched_wc_refund = null; /** - * Get refunds from order and delete refund if matches wcpay refund id. + * Get the WC_Refund from the WCPay refund ID. * * @var $wc_refunds WC_Order_Refund[] * */ @@ -304,34 +283,33 @@ private function process_webhook_refund_updated( $event_body ) { foreach ( $wc_refunds as $wc_refund ) { $wcpay_refund_id = $this->order_service->get_wcpay_refund_id_for_order( $wc_refund ); if ( $refund_id === $wcpay_refund_id ) { - // Delete WC Refund. - $wc_refund->delete(); + $matched_wc_refund = $wc_refund; break; } } } - // Update order status if order is fully refunded. - $current_order_status = $order->get_status(); - if ( Order_Status::REFUNDED === $current_order_status ) { - $order->update_status( Order_Status::FAILED ); - } - - $order->add_order_note( $note ); - $this->order_service->set_wcpay_refund_status_for_order( $order, 'failed' ); - $order->save(); - - try { - $failure_reason = $this->read_webhook_property( $event_object, 'failure_reason' ); - - if ( 'insufficient_funds' === $failure_reason ) { - $this->order_service->handle_insufficient_balance_for_refund( - $order, - $amount - ); - } - } catch ( Exception $e ) { - Logger::debug( 'Failed to handle insufficient balance for refund: ' . $e->getMessage() ); + // Refund update webhook events can be either failed, cancelled (basically it's also a failure but triggered by the merchant), succeeded only. + switch ( $status ) { + case Refund_Status::FAILED: + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund ); + if ( + $this->has_webhook_property( $event_object, 'failure_reason' ) + && 'insufficient_funds' === $this->read_webhook_property( $event_object, 'failure_reason' ) + ) { + $this->order_service->handle_insufficient_balance_for_refund( $order, $amount ); + } + break; + case Refund_Status::CANCELED: + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund, true ); + break; + case Refund_Status::SUCCEEDED: + if ( $matched_wc_refund ) { + $this->order_service->add_note_and_metadata_for_created_refund( $order, $matched_wc_refund, $refund_id, $balance_transaction ?? null ); + } + break; + default: + throw new Invalid_Webhook_Data_Exception( 'Invalid refund update status: ' . $status ); } } @@ -470,11 +448,10 @@ private function process_webhook_payment_intent_succeeded( $event_body ) { $intent_id = $this->read_webhook_property( $event_object, 'id' ); $currency = $this->read_webhook_property( $event_object, 'currency' ); $order = $this->get_order_from_event_body( $event_body ); - $intent_status = $this->read_webhook_property( $event_object, 'status' ); $event_charges = $this->read_webhook_property( $event_object, 'charges' ); $charges_data = $this->read_webhook_property( $event_charges, 'data' ); $charge_id = $this->read_webhook_property( $charges_data[0], 'id' ); - $metadata = $this->read_webhook_property( $event_object, 'metadata' ); + $charge_amount = $this->read_webhook_property( $event_object, 'amount' ); $payment_method_id = $charges_data[0]['payment_method'] ?? null; if ( ! $order ) { @@ -496,8 +473,13 @@ private function process_webhook_payment_intent_succeeded( $event_body ) { } $application_fee_amount = $charges_data[0]['application_fee_amount'] ?? null; + if ( $application_fee_amount ) { - $meta_data_to_update['_wcpay_transaction_fee'] = WC_Payments_Utils::interpret_stripe_amount( $application_fee_amount, $currency ); + $fee = WC_Payments_Utils::interpret_stripe_amount( $application_fee_amount, $currency ); + $meta_data_to_update['_wcpay_transaction_fee'] = $fee; + + $charge_amount = WC_Payments_Utils::interpret_stripe_amount( $charge_amount, $currency ); + $meta_data_to_update['_wcpay_net'] = $charge_amount - $fee; } foreach ( $meta_data_to_update as $key => $value ) { @@ -879,7 +861,7 @@ private function process_webhook_refund_triggered_externally( array $event_body $wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, ( ! $is_partial_refund ? $order->get_items() : [] ) ); // Process the refund in the order service. - $this->order_service->add_note_and_metadata_for_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id, Refund_Status::PENDING === $refund['status'] ); } /** diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 41759213ddf..e5b120f59aa 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -445,6 +445,7 @@ public static function init() { include_once __DIR__ . '/payment-methods/class-multibanco-payment-method.php'; include_once __DIR__ . '/payment-methods/class-grabpay-payment-method.php'; include_once __DIR__ . '/payment-methods/class-wechatpay-payment-method.php'; + include_once __DIR__ . '/inline-script-payloads/class-woo-payments-payment-methods-config.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-helper.php'; include_once __DIR__ . '/class-wc-payment-token-wcpay-sepa.php'; include_once __DIR__ . '/class-wc-payments-status.php'; @@ -471,6 +472,7 @@ public static function init() { include_once __DIR__ . '/exceptions/class-order-id-mismatch-exception.php'; include_once __DIR__ . '/exceptions/class-rate-limiter-enabled-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-address-exception.php'; + include_once __DIR__ . '/exceptions/class-subscription-mode-mismatch-exception.php'; include_once __DIR__ . '/constants/class-base-constant.php'; include_once __DIR__ . '/constants/class-country-code.php'; include_once __DIR__ . '/constants/class-country-test-cards.php'; @@ -481,6 +483,7 @@ public static function init() { include_once __DIR__ . '/constants/class-payment-type.php'; include_once __DIR__ . '/constants/class-payment-initiated-by.php'; include_once __DIR__ . '/constants/class-intent-status.php'; + include_once __DIR__ . '/constants/class-refund-status.php'; include_once __DIR__ . '/constants/class-payment-intent-status.php'; include_once __DIR__ . '/constants/class-payment-capture-type.php'; include_once __DIR__ . '/constants/class-payment-method.php'; @@ -568,6 +571,10 @@ public static function init() { self::$customer_service->init_hooks(); self::$token_service->init_hooks(); + /** + * FLAG: PAYMENT_METHODS_LIST + * As payment methods are converted to use definitions, they need to be removed from the list below. + */ $payment_method_classes = [ CC_Payment_Method::class, Bancontact_Payment_Method::class, @@ -676,7 +683,6 @@ function () { add_filter( 'woocommerce_payment_gateways', [ __CLASS__, 'register_gateway' ] ); add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'order_woopayments_gateways' ], 2 ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'order_woopayments_gateways' ], 3 ); - add_filter( 'woocommerce_rest_api_option_permissions', [ __CLASS__, 'add_wcpay_options_to_woocommerce_permissions_list' ], 5 ); add_filter( 'woocommerce_admin_get_user_data_fields', [ __CLASS__, 'add_user_data_fields' ] ); // Add note query support for source. @@ -1119,6 +1125,10 @@ public static function init_rest_api() { $settings_controller = new WC_REST_Payments_Settings_Controller( self::$api_client, self::get_gateway(), self::$account ); $settings_controller->register_routes(); + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-settings-option-controller.php'; + $settings_option_controller = new WC_REST_Payments_Settings_Option_Controller( self::$api_client ); + $settings_option_controller->register_routes(); + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-reader-controller.php'; $charges_controller = new WC_REST_Payments_Reader_Controller( self::$api_client, self::get_gateway(), self::$in_person_payments_receipts_service ); $charges_controller->register_routes(); @@ -1957,45 +1967,6 @@ public static function enqueue_dev_runtime_scripts() { } } - /** - * Adds WCPay options to Woo Core option allow list. - * - * @param array $permissions Array containing the permissions. - * - * @return array An array containing the modified permissions. - */ - public static function add_wcpay_options_to_woocommerce_permissions_list( $permissions ) { - $wcpay_permissions_list = array_fill_keys( - [ - 'wcpay_frt_discover_banner_settings', - 'wcpay_multi_currency_setup_completed', - 'woocommerce_dismissed_todo_tasks', - 'woocommerce_remind_me_later_todo_tasks', - 'woocommerce_deleted_todo_tasks', - 'wcpay_fraud_protection_welcome_tour_dismissed', - 'wcpay_capability_request_dismissed_notices', - 'wcpay_onboarding_eligibility_modal_dismissed', - 'wcpay_connection_success_modal_dismissed', - 'wcpay_next_deposit_notice_dismissed', - 'wcpay_duplicate_payment_method_notices_dismissed', - 'wcpay_exit_survey_dismissed', - 'wcpay_instant_deposit_notice_dismissed', - 'wcpay_date_format_notice_dismissed', - ], - true - ); - - if ( is_array( $permissions ) ) { - return array_merge( - $permissions, - $wcpay_permissions_list - ); - } - - return $wcpay_permissions_list; - } - - /** * Creates a new request object for a server call. * diff --git a/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php b/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php index 7d4838bfceb..324c7e36119 100644 --- a/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php +++ b/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php @@ -16,7 +16,26 @@ } /** + * Class WC_Payments_Email_Failed_Authentication_Retry + * * An email sent to the admin when payment fails to go through due to authentication_required error. + * + * @extends WC_Email_Failed_Order + * + * @filter woocommerce_email_preview_dummy_order + * Filters the dummy order object used for email previews. + * @param WC_Order|bool $order The order object or false. + * @return WC_Order The filtered order object. + * + * @filter woocommerce_email_preview_dummy_retry + * Filters the dummy retry object used for email previews. + * @param WCS_Retry|bool $retry The retry object or false. + * @return WCS_Retry|null The filtered retry object or null if WCS_Retry class doesn't exist. + * + * @filter woocommerce_email_preview_placeholders + * Filters the email preview placeholders. + * @param array $placeholders Array of email preview placeholders. + * @return array Modified array of placeholders. */ class WC_Payments_Email_Failed_Authentication_Retry extends WC_Email_Failed_Order { @@ -46,6 +65,69 @@ public function __construct() { // We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor. WC_Email::__construct(); + + // Add email preview filters. + add_filter( 'woocommerce_email_preview_dummy_order', [ $this, 'get_preview_order' ], 10, 1 ); + add_filter( 'woocommerce_email_preview_dummy_retry', [ $this, 'get_preview_retry' ], 10, 1 ); + add_filter( 'woocommerce_email_preview_placeholders', [ $this, 'get_preview_placeholders' ], 10, 1 ); + } + + /** + * Get a dummy order for email preview. + * + * @param WC_Order|bool $order The order object or false. + * @return WC_Order + */ + public function get_preview_order( $order ) { + if ( ! $order instanceof WC_Order ) { + $order = wc_create_order(); + $order->set_status( 'failed' ); + $order->set_billing_first_name( 'John' ); + $order->set_billing_last_name( 'Doe' ); + $order->set_billing_email( 'john.doe@example.com' ); + $order->set_total( 99.99 ); + $order->save(); + } + return $order; + } + + /** + * Get a dummy retry object for email preview. + * + * @param WCS_Retry|bool $retry The retry object or false. + * @return WCS_Retry|null + */ + public function get_preview_retry( $retry ) { + if ( ! class_exists( 'WCS_Retry' ) || ! function_exists( 'wcs_get_human_time_diff' ) ) { + return null; + } + + if ( ! $retry instanceof WCS_Retry ) { + $retry_data = [ + 'time' => time() + DAY_IN_SECONDS, + 'order_id' => 0, + 'retry_number' => 1, + 'status' => 'pending', + ]; + $retry = new WCS_Retry( $retry_data ); + } + return $retry; + } + + /** + * Get preview placeholders. + * + * @param array $placeholders The placeholders array. + * @return array + */ + public function get_preview_placeholders( $placeholders ) { + $retry = $this->get_preview_retry( false ); + $retry_time = ''; + if ( $retry && function_exists( 'wcs_get_human_time_diff' ) ) { + $retry_time = wcs_get_human_time_diff( $retry->get_time() ); + } + $placeholders['{retry_time}'] = $retry_time; + return $placeholders; } /** @@ -73,23 +155,29 @@ public function get_default_heading() { * @param WC_Order|null $order Order object. */ public function trigger( $order_id, $order = null ) { + if ( ! $this->is_enabled() || ! $this->get_recipient() ) { + return; + } + $this->object = $order; - $this->find['retry-time'] = '{retry_time}'; if ( class_exists( 'WCS_Retry_Manager' ) && function_exists( 'wcs_get_human_time_diff' ) ) { - $this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) ); - $this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() ); + $this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) ); } else { Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admin email about customer notification for authentication required for renewal payment.' ); return; } + // Set up order number replacement. $this->find['order-number'] = '{order_number}'; $this->replace['order-number'] = $this->object->get_order_number(); - if ( ! $this->is_enabled() || ! $this->get_recipient() ) { - return; + // Set up retry time replacement. + $retry_time = ''; + if ( $this->retry && function_exists( 'wcs_get_human_time_diff' ) ) { + $retry_time = wcs_get_human_time_diff( $this->retry->get_time() ); } + $this->placeholders['{retry_time}'] = $retry_time; $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } @@ -100,6 +188,11 @@ public function trigger( $order_id, $order = null ) { * @return string */ public function get_content_html() { + // Ensure retry object is initialized for preview. + if ( ! isset( $this->retry ) ) { + $this->retry = $this->get_preview_retry( false ); + } + return wc_get_template_html( $this->template_html, [ @@ -121,6 +214,11 @@ public function get_content_html() { * @return string */ public function get_content_plain() { + // Ensure retry object is initialized for preview. + if ( ! isset( $this->retry ) ) { + $this->retry = $this->get_preview_retry( false ); + } + return wc_get_template_html( $this->template_plain, [ diff --git a/includes/constants/class-payment-method.php b/includes/constants/class-payment-method.php index 24c5cb8b831..cd354fbdc3d 100644 --- a/includes/constants/class-payment-method.php +++ b/includes/constants/class-payment-method.php @@ -18,6 +18,10 @@ * @psalm-immutable */ class Payment_Method extends Base_Constant { + /** + * FLAG: PAYMENT_METHODS_LIST + * We need to see how we can use the definitions to replace these constants. + */ const ALIPAY = 'alipay'; const BANCONTACT = 'bancontact'; const BASC = 'bacs_debit'; diff --git a/includes/constants/class-refund-status.php b/includes/constants/class-refund-status.php new file mode 100644 index 00000000000..a0a16d8fa3c --- /dev/null +++ b/includes/constants/class-refund-status.php @@ -0,0 +1,24 @@ +set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) ); + return $order; + } + + /** + * Get preview address data for email preview. + * + * @param array $address The address data. + * @return array + */ + public function get_preview_address( $address ) { + if ( empty( $address ) ) { + $address = [ + 'line1' => '123 Sample Street', + 'line2' => 'Suite 100', + 'city' => 'Sample City', + 'state' => 'ST', + 'postal_code' => '12345', + 'country' => 'US', + ]; + } + return $address; + } + + /** + * Get preview placeholders for email preview. + * + * @param array $placeholders The placeholders array. + * @return array + */ + public function get_preview_placeholders( $placeholders ) { + $placeholders['{order_date}'] = wc_format_datetime( new DateTime() ); + $placeholders['{order_number}'] = '42'; + return $placeholders; + } + /** * Get email subject. * @@ -171,6 +235,33 @@ public function get_content_plain(): string { ); } + /** + * Get preview merchant settings for email preview. + * + * @param array $settings The merchant settings. + * @return array + */ + public function get_preview_merchant_settings( $settings ) { + if ( empty( $settings ) ) { + $settings = [ + 'business_name' => 'Sample Store', + 'support_info' => [ + 'address' => [ + 'line1' => '123 Sample Street', + 'line2' => 'Suite 100', + 'city' => 'Sample City', + 'state' => 'ST', + 'postal_code' => '12345', + 'country' => 'US', + ], + 'phone' => '+1 (555) 123-4567', + 'email' => 'support@samplestore.com', + ], + ]; + } + return $settings; + } + /** * Get store details content html * @@ -179,34 +270,58 @@ public function get_content_plain(): string { * @return void */ public function store_details( array $settings, bool $plain_text ) { + // Ensure we have all required data for preview. + $settings = $this->get_preview_merchant_settings( $settings ); + + $template_data = [ + 'business_name' => $settings['business_name'] ?? '', + 'support_address' => $settings['support_info']['address'] ?? [], + 'support_phone' => $settings['support_info']['phone'] ?? '', + 'support_email' => $settings['support_info']['email'] ?? '', + ]; + if ( $plain_text ) { wc_get_template( 'emails/plain/email-ipp-receipt-store-details.php', - [ - 'business_name' => $settings['business_name'], - 'support_address' => $settings['support_info']['address'], - 'support_phone' => $settings['support_info']['phone'], - 'support_email' => $settings['support_info']['email'], - ], + $template_data, '', WCPAY_ABSPATH . 'templates/' ); - } else { wc_get_template( 'emails/email-ipp-receipt-store-details.php', - [ - 'business_name' => $settings['business_name'], - 'support_address' => $settings['support_info']['address'], - 'support_phone' => $settings['support_info']['phone'], - 'support_email' => $settings['support_info']['email'], - ], + $template_data, '', WCPAY_ABSPATH . 'templates/' ); } } + /** + * Get preview charge data for email preview. + * + * @param array $charge The charge data. + * @return array + */ + public function get_preview_charge( $charge ) { + if ( empty( $charge ) ) { + $charge = [ + 'payment_method_details' => [ + 'card_present' => [ + 'brand' => 'visa', + 'last4' => '4242', + 'receipt' => [ + 'application_preferred_name' => 'Sample App', + 'dedicated_file_name' => 'Sample File', + 'account_type' => 'credit', + ], + ], + ], + ]; + } + return $charge; + } + /** * Get compliance data content html * @@ -215,23 +330,25 @@ public function store_details( array $settings, bool $plain_text ) { * @return void */ public function compliance_details( array $charge, bool $plain_text ) { + // Ensure we have all required data for preview. + $charge = $this->get_preview_charge( $charge ); + + $template_data = [ + 'payment_method_details' => $charge['payment_method_details']['card_present'] ?? [], + 'receipt' => $charge['payment_method_details']['card_present']['receipt'] ?? [], + ]; + if ( $plain_text ) { wc_get_template( 'emails/plain/email-ipp-receipt-compliance-details.php', - [ - 'payment_method_details' => $charge['payment_method_details']['card_present'], - 'receipt' => $charge['payment_method_details']['card_present']['receipt'], - ], + $template_data, '', WCPAY_ABSPATH . 'templates/' ); } else { wc_get_template( 'emails/email-ipp-receipt-compliance-details.php', - [ - 'payment_method_details' => $charge['payment_method_details']['card_present'], - 'receipt' => $charge['payment_method_details']['card_present']['receipt'], - ], + $template_data, '', WCPAY_ABSPATH . 'templates/' ); diff --git a/includes/exceptions/class-subscription-mode-mismatch-exception.php b/includes/exceptions/class-subscription-mode-mismatch-exception.php new file mode 100644 index 00000000000..5045485985d --- /dev/null +++ b/includes/exceptions/class-subscription-mode-mismatch-exception.php @@ -0,0 +1,29 @@ +get_route() === '/wc/store/v1/cart/update-customer'; if ( $is_update_customer_route ) { add_filter( 'woocommerce_validate_postcode', [ $this, 'maybe_skip_postcode_validation' ], 10, 3 ); } - $request_data = $request->get_json_params(); - if ( isset( $request_data['shipping_address'] ) ) { - $request->set_param( 'shipping_address', $this->transform_ece_address_state_data( $request_data['shipping_address'] ) ); - // on the "update customer" route, GooglePay/Apple pay might provide redacted postcode data. + if ( isset( $request['shipping_address'] ) && is_array( $request['shipping_address'] ) ) { + $shipping_address = $request['shipping_address']; + $shipping_address = $this->transform_ece_address_state_data( $shipping_address ); + // on the "update customer" route, Google Pay/Apple Pay might provide redacted postcode data. // we need to modify the zip code to ensure that shipping zone identification still works. if ( $is_update_customer_route ) { - $request->set_param( 'shipping_address', $this->transform_ece_address_postcode_data( $request_data['shipping_address'] ) ); + $shipping_address = $this->transform_ece_address_postcode_data( $shipping_address ); } + $request->set_param( 'shipping_address', $shipping_address ); } - if ( isset( $request_data['billing_address'] ) ) { - $request->set_param( 'billing_address', $this->transform_ece_address_state_data( $request_data['billing_address'] ) ); - // on the "update customer" route, GooglePay/Apple pay might provide redacted postcode data. + if ( isset( $request['billing_address'] ) && is_array( $request['billing_address'] ) ) { + $billing_address = $request['billing_address']; + $billing_address = $this->transform_ece_address_state_data( $billing_address ); + // on the "update customer" route, Google Pay/Apple Pay might provide redacted postcode data. // we need to modify the zip code to ensure that shipping zone identification still works. if ( $is_update_customer_route ) { - $request->set_param( 'billing_address', $this->transform_ece_address_postcode_data( $request_data['billing_address'] ) ); + $billing_address = $this->transform_ece_address_postcode_data( $billing_address ); } + $request->set_param( 'billing_address', $billing_address ); } return $response; @@ -561,9 +564,9 @@ public function maybe_skip_postcode_validation( $valid, $postcode, $country ) { } /** - * Transform a GooglePay/ApplePay state address data fields into values that are valid for WooCommerce. + * Transform a Google Pay/Apple Pay state address data fields into values that are valid for WooCommerce. * - * @param array $address The address to normalize from the GooglePay/ApplePay request. + * @param array $address The address to normalize from the Google Pay/Apple Pay request. * * @return array */ @@ -583,9 +586,9 @@ private function transform_ece_address_state_data( $address ) { } /** - * Transform a GooglePay/ApplePay postcode address data fields into values that are valid for WooCommerce. + * Transform a Google Pay/Apple Pay postcode address data fields into values that are valid for WooCommerce. * - * @param array $address The address to normalize from the GooglePay/ApplePay request. + * @param array $address The address to normalize from the Google Pay/Apple Pay request. * * @return array */ diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php index f9c978d43a1..b13244d3ae1 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php @@ -141,11 +141,10 @@ public function display_express_checkout_buttons() { } else { $this->add_order_attribution_inputs(); } - + $this->display_express_checkout_separator_if_necessary( $separator_starts_hidden ); ?>
        display_express_checkout_separator_if_necessary( $separator_starts_hidden ); } } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php index 073e09b0ad9..c224250b9cb 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php @@ -250,6 +250,7 @@ public function get_express_checkout_params() { // Defaults to 'required' to match how core initializes this option. 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), 'allowed_shipping_countries' => array_keys( WC()->countries->get_shipping_countries() ?? [] ), + 'display_prices_with_tax' => 'incl' === get_option( 'woocommerce_tax_display_cart' ), ], 'button' => $this->get_button_settings(), 'login_confirmation' => $this->get_login_confirmation_settings(), @@ -329,7 +330,7 @@ public function display_express_checkout_button_html() { return; } ?> - +
        get_cached_account_data(); + $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : ''; + $payment_method_map = \WC_Payments::get_payment_method_map(); + $payment_method_information_object = []; + + foreach ( $payment_method_map as $id => $payment_method ) { + $payment_method_information_object[ $id ] = + $payment_method->get_payment_method_information_object( $account_country ); + } + + $payment_method_information_object = rawurlencode( wp_json_encode( $payment_method_information_object ) ); return " - window.wooPaymentsPaymentMethodDefinitions = JSON.parse( decodeURIComponent( '" . esc_js( $payment_method_definitions ) . "' ) ); + window.wooPaymentsPaymentMethodDefinitions = JSON.parse( decodeURIComponent( '" . esc_js( $payment_method_information_object ) . "' ) ); "; } } diff --git a/includes/inline-script-payloads/class-woo-payments-payment-methods-config.php b/includes/inline-script-payloads/class-woo-payments-payment-methods-config.php new file mode 100644 index 00000000000..8ed4ee7f32e --- /dev/null +++ b/includes/inline-script-payloads/class-woo-payments-payment-methods-config.php @@ -0,0 +1,31 @@ +get_all_payment_method_config() ) ); + + return " + window.wooPaymentsPaymentMethodsConfig = JSON.parse( decodeURIComponent( '" . esc_js( $payment_methods_config ) . "' ) ); + "; + } +} diff --git a/includes/multi-currency/Compatibility/WooCommerceBookings.php b/includes/multi-currency/Compatibility/WooCommerceBookings.php index bb6bc4bb667..deb64935acb 100644 --- a/includes/multi-currency/Compatibility/WooCommerceBookings.php +++ b/includes/multi-currency/Compatibility/WooCommerceBookings.php @@ -53,6 +53,8 @@ public function init() { add_filter( 'woocommerce_product_get_resource_base_costs', [ $this, 'get_resource_prices' ], 50, 1 ); add_filter( 'woocommerce_product_get_resource_block_costs', [ $this, 'get_resource_prices' ], 50, 1 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 ); + add_filter( 'woocommerce_bookings_process_cost_rules_cost', [ $this, 'get_price' ], 50, 1 ); + add_filter( 'woocommerce_bookings_process_cost_rules_base_cost', [ $this, 'get_price' ], 50, 1 ); add_action( 'wp_ajax_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 ); add_action( 'wp_ajax_nopriv_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 ); } diff --git a/includes/multi-currency/client/data/actions.js b/includes/multi-currency/client/data/actions.js index 0b822dde4cb..00cc50b7e6d 100644 --- a/includes/multi-currency/client/data/actions.js +++ b/includes/multi-currency/client/data/actions.js @@ -4,6 +4,7 @@ * External dependencies */ import { apiFetch } from '@wordpress/data-controls'; +import directApiFetch from '@wordpress/api-fetch'; import { dispatch, select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -145,3 +146,15 @@ export function* submitStoreSettingsUpdate( ); } } + +export function saveOption( optionName, value ) { + directApiFetch( { + path: `${ NAMESPACE }/settings/${ optionName }`, + method: 'post', + data: { value }, + } ).catch( () => { + dispatch( 'core/notices' ).createErrorNotice( + __( 'Error saving option', 'woocommerce-payments' ) + ); + } ); +} diff --git a/includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/test/__snapshots__/index.js.snap b/includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/test/__snapshots__/index.js.snap index 982e77f82a9..ba1b773793e 100644 --- a/includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/test/__snapshots__/index.js.snap +++ b/includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/test/__snapshots__/index.js.snap @@ -365,12 +365,12 @@ exports[`Multi-Currency enabled currencies list Remove currency modal renders co Giropay - giropay + Giropay
      • @@ -381,7 +381,7 @@ exports[`Multi-Currency enabled currencies list Remove currency modal renders co Sofort { const { isActive } = useContext( WizardTaskContext ); const defaultCurrency = useDefaultCurrency(); - const { updateOptions } = useDispatch( 'wc/admin/options' ); useEffect( () => { if ( ! isActive ) { return; } - updateOptions( { - // eslint-disable-next-line camelcase - wcpay_multi_currency_setup_completed: 'yes', - } ); + saveOption( 'wcpay_multi_currency_setup_completed', true ); // Set the local `isSetupCompleted` to `yes` so that task appears completed on the list. // Please note that marking an item as "completed" is different from "dismissing" it. window.wcpaySettings.multiCurrencySetup.isSetupCompleted = 'yes'; - }, [ isActive, updateOptions ] ); + }, [ isActive ] ); return ( ( { - useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), +jest.mock( 'multi-currency/data/actions', () => ( { + saveOption: jest.fn(), } ) ); jest.mock( 'multi-currency/interface/data', () => ( {} ) ); diff --git a/includes/payment-methods/Configs/Definitions/AlipayDefinition.php b/includes/payment-methods/Configs/Definitions/AlipayDefinition.php index ec5ca85536b..f286d295dac 100644 --- a/includes/payment-methods/Configs/Definitions/AlipayDefinition.php +++ b/includes/payment-methods/Configs/Definitions/AlipayDefinition.php @@ -28,7 +28,7 @@ public static function get_id(): string { } /** - * Get the keywords for the payment method + * Get the keywords for the payment method. These are used by the duplicates detection service. * * @return string[] */ @@ -56,12 +56,24 @@ public static function get_title( ?string $account_country = null ): string { return __( 'Alipay', 'woocommerce-payments' ); } + /** + * Get the title of the payment method for the settings page. + * + * @param string|null $account_country Optional. The merchant's account country. + * + * @return string + */ + public static function get_settings_label( ?string $account_country = null ): string { + return self::get_title( $account_country ); + } + /** * Get the customer-facing description of the payment method * + * @param string|null $account_country Optional. The merchant's account country. * @return string */ - public static function get_description(): string { + public static function get_description( ?string $account_country = null ): string { return __( 'Alipay is a popular wallet in China, operated by Ant Financial Services Group, a financial services provider affiliated with Alibaba.', 'woocommerce-payments' ); } @@ -232,10 +244,12 @@ public static function get_dark_icon_url( ?string $account_country = null ): str /** * Get the URL for the payment method's settings icon * + * @param string|null $account_country Optional. The merchant's account country. + * * @return string */ - public static function get_settings_icon_url(): string { - return self::get_icon_url(); + public static function get_settings_icon_url( ?string $account_country = null ): string { + return self::get_icon_url( $account_country ); } /** diff --git a/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php b/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php index 234a825ab48..7f1f9849b99 100644 --- a/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php +++ b/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php @@ -20,7 +20,7 @@ interface PaymentMethodDefinitionInterface { public static function get_id(): string; /** - * Get the keywords for the payment method + * Get the keywords for the payment method. These are used by the duplicates detection service. * * @return string[] */ @@ -41,12 +41,21 @@ public static function get_stripe_id(): string; */ public static function get_title( ?string $account_country = null ): string; + /** + * Get the title of the payment method for the settings page. + * + * @param string|null $account_country Optional. The merchant's account country. + * @return string + */ + public static function get_settings_label( ?string $account_country = null ): string; + /** * Get the customer-facing description of the payment method * + * @param string|null $account_country Optional. The merchant's account country. * @return string */ - public static function get_description(): string; + public static function get_description( ?string $account_country = null ): string; /** * Is the payment method a BNPL (Buy Now Pay Later) payment method? @@ -120,9 +129,10 @@ public static function get_dark_icon_url( ?string $account_country = null ): str * Get the URL for the payment method's settings icon * This icon is used in the payment method settings page. * + * @param string|null $account_country Optional. The merchant's account country. * @return string */ - public static function get_settings_icon_url(): string; + public static function get_settings_icon_url( ?string $account_country = null ): string; /** * Get the testing instructions for the payment method diff --git a/includes/payment-methods/class-affirm-payment-method.php b/includes/payment-methods/class-affirm-payment-method.php index 1c87c67149f..05c6aa8609e 100644 --- a/includes/payment-methods/class-affirm-payment-method.php +++ b/includes/payment-methods/class-affirm-payment-method.php @@ -58,4 +58,28 @@ public function get_title( ?string $account_country = null, $payment_details = f public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Allow customers to pay over time with Affirm.', + 'woocommerce-payments' + ); + } + + /** + * Returns payment method settings icon. + * + * @param string|null $account_country Country of merchants account. + * @return string + */ + public function get_settings_icon_url( ?string $account_country = null ) { + return plugins_url( 'assets/images/payment-methods/affirm-badge.svg', WCPAY_PLUGIN_FILE ); + } } diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php index 503f0c6104d..41dbb955e3e 100644 --- a/includes/payment-methods/class-afterpay-payment-method.php +++ b/includes/payment-methods/class-afterpay-payment-method.php @@ -46,10 +46,14 @@ public function __construct( $token_service ) { * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ public function get_title( ?string $account_country = null, $payment_details = false ) { - if ( 'GB' === $account_country ) { + if ( Country_Code::UNITED_KINGDOM === $account_country ) { return __( 'Clearpay', 'woocommerce-payments' ); } + if ( Country_Code::UNITED_STATES === $account_country ) { + return __( 'Cash App Afterpay', 'woocommerce-payments' ); + } + return __( 'Afterpay', 'woocommerce-payments' ); } @@ -60,13 +64,35 @@ public function get_title( ?string $account_country = null, $payment_details = f * @return string|null */ public function get_icon( ?string $account_country = null ) { - if ( 'GB' === $account_country ) { + if ( Country_Code::UNITED_KINGDOM === $account_country ) { return plugins_url( 'assets/images/payment-methods/clearpay.svg', WCPAY_PLUGIN_FILE ); } + if ( Country_Code::UNITED_STATES === $account_country ) { + return plugins_url( 'assets/images/payment-methods/afterpay-cashapp-logo.svg', WCPAY_PLUGIN_FILE ); + } + return plugins_url( 'assets/images/payment-methods/afterpay-badge.svg', WCPAY_PLUGIN_FILE ); } + /** + * Returns payment method dark icon. + * + * @param string|null $account_country Country of merchants account. + * @return string|null + */ + public function get_dark_icon( ?string $account_country = null ) { + if ( Country_Code::UNITED_KINGDOM === $account_country ) { + return plugins_url( 'assets/images/payment-methods/clearpay-dark.svg', WCPAY_PLUGIN_FILE ); + } + + if ( Country_Code::UNITED_STATES === $account_country ) { + return plugins_url( 'assets/images/payment-methods/afterpay-cashapp-logo-dark.svg', WCPAY_PLUGIN_FILE ); + } + + return plugins_url( 'assets/images/payment-methods/afterpay-badge-dark.svg', WCPAY_PLUGIN_FILE ); + } + /** * Returns testing credentials to be printed at checkout in test mode. * @@ -76,4 +102,43 @@ public function get_icon( ?string $account_country = null ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + if ( Country_Code::UNITED_KINGDOM === $account_country ) { + return __( + 'Allow customers to pay over time with Clearpay.', + 'woocommerce-payments' + ); + } + + return __( + 'Allow customers to pay over time with Afterpay.', + 'woocommerce-payments' + ); + } + + /** + * Returns payment method settings icon. + * + * @param string|null $account_country Country of merchants account. + * @return string + */ + public function get_settings_icon_url( ?string $account_country = null ) { + if ( Country_Code::UNITED_KINGDOM === $account_country ) { + return plugins_url( 'assets/images/payment-methods/clearpay.svg', WCPAY_PLUGIN_FILE ); + } + + if ( Country_Code::UNITED_STATES === $account_country ) { + return plugins_url( 'assets/images/payment-methods/afterpay-cashapp-badge.svg', WCPAY_PLUGIN_FILE ); + } + + return plugins_url( 'assets/images/payment-methods/afterpay-logo.svg', WCPAY_PLUGIN_FILE ); + } } diff --git a/includes/payment-methods/class-bancontact-payment-method.php b/includes/payment-methods/class-bancontact-payment-method.php index 422636084ff..3b5ab9bf42f 100644 --- a/includes/payment-methods/class-bancontact-payment-method.php +++ b/includes/payment-methods/class-bancontact-payment-method.php @@ -42,4 +42,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Bancontact is a bank redirect payment method offered by more than 80% of online businesses in Belgium.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-becs-payment-method.php b/includes/payment-methods/class-becs-payment-method.php index 763a181d059..b26c5c7d760 100644 --- a/includes/payment-methods/class-becs-payment-method.php +++ b/includes/payment-methods/class-becs-payment-method.php @@ -42,4 +42,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return __( 'Test mode: use the test account number 000123456. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Bulk Electronic Clearing System — Accept secure bank transfer from Australia.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php index 9e7f3ba6860..7ec20418002 100644 --- a/includes/payment-methods/class-cc-payment-method.php +++ b/includes/payment-methods/class-cc-payment-method.php @@ -79,4 +79,47 @@ public function get_testing_instructions( string $account_country ) { $test_card_number ); } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Let your customers pay with major credit and debit cards without leaving your store.', + 'woocommerce-payments' + ); + } + + /** + * Returns payment method settings label. + * + * @param string $account_country Country of merchants account. + * @return string + */ + public function get_settings_label( string $account_country ) { + return __( 'Credit / Debit Cards', 'woocommerce-payments' ); + } + + /** + * Returns payment method settings icon. + * + * @param string|null $account_country Country of merchants account. + * @return string + */ + public function get_settings_icon_url( ?string $account_country = null ) { + return plugins_url( 'assets/images/payment-methods/cc.svg', WCPAY_PLUGIN_FILE ); + } + + /** + * Returns boolean dependent on whether payment method allows manual capture. + * + * @return bool + */ + public function allows_manual_capture() { + return true; + } } diff --git a/includes/payment-methods/class-eps-payment-method.php b/includes/payment-methods/class-eps-payment-method.php index ab936ace70d..314e2f4432b 100644 --- a/includes/payment-methods/class-eps-payment-method.php +++ b/includes/payment-methods/class-eps-payment-method.php @@ -42,4 +42,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Accept your payment with EPS — a common payment method in Austria.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-giropay-payment-method.php b/includes/payment-methods/class-giropay-payment-method.php index 82095ffbf33..8348f0964bf 100644 --- a/includes/payment-methods/class-giropay-payment-method.php +++ b/includes/payment-methods/class-giropay-payment-method.php @@ -42,4 +42,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Expand your business with giropay — Germany’s second most popular payment system.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-grabpay-payment-method.php b/includes/payment-methods/class-grabpay-payment-method.php index 099fa173df7..2b17ad5e147 100644 --- a/includes/payment-methods/class-grabpay-payment-method.php +++ b/includes/payment-methods/class-grabpay-payment-method.php @@ -57,4 +57,18 @@ public function get_title( ?string $account_country = null, $payment_details = f public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'A popular digital wallet for cashless payments in Singapore.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-ideal-payment-method.php b/includes/payment-methods/class-ideal-payment-method.php index 5c1ac9abfbe..81796f2b455 100644 --- a/includes/payment-methods/class-ideal-payment-method.php +++ b/includes/payment-methods/class-ideal-payment-method.php @@ -42,4 +42,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Expand your business with iDEAL — Netherlands’s most popular payment method.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-klarna-payment-method.php b/includes/payment-methods/class-klarna-payment-method.php index 1f40314d44f..5648c34ca7e 100644 --- a/includes/payment-methods/class-klarna-payment-method.php +++ b/includes/payment-methods/class-klarna-payment-method.php @@ -70,6 +70,11 @@ public function get_countries() { if ( in_array( $account_country, $eea_countries, true ) ) { $store_currency = strtoupper( get_woocommerce_currency() ); + // if the store is set to an EU country, but the currency used is not set as a valid EU currency, I guess Klarna shouldn't be eligible. + if ( ! isset( $this->limits_per_currency[ $store_currency ] ) ) { + return [ 'NONE_SUPPORTED' ]; + } + $countries_that_support_store_currency = array_keys( $this->limits_per_currency[ $store_currency ] ); return array_values( array_intersect( $eea_countries, $countries_that_support_store_currency ) ); @@ -87,4 +92,28 @@ public function get_countries() { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Allow customers to pay over time or pay now with Klarna.', + 'woocommerce-payments' + ); + } + + /** + * Returns payment method settings icon. + * + * @param string|null $account_country Country of merchants account. + * @return string + */ + public function get_settings_icon_url( ?string $account_country = null ) { + return plugins_url( 'assets/images/payment-methods/klarna.svg', WCPAY_PLUGIN_FILE ); + } } diff --git a/includes/payment-methods/class-link-payment-method.php b/includes/payment-methods/class-link-payment-method.php index 0e086cd7e86..def40a86912 100644 --- a/includes/payment-methods/class-link-payment-method.php +++ b/includes/payment-methods/class-link-payment-method.php @@ -51,4 +51,16 @@ public function get_title( ?string $account_country = null, $payment_details = f public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + // Description is hardcoded in the react component. + return ''; + } } diff --git a/includes/payment-methods/class-multibanco-payment-method.php b/includes/payment-methods/class-multibanco-payment-method.php index 4dda05318bf..8ae349257da 100644 --- a/includes/payment-methods/class-multibanco-payment-method.php +++ b/includes/payment-methods/class-multibanco-payment-method.php @@ -55,4 +55,28 @@ public function get_title( ?string $account_country = null, $payment_details = f public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'A voucher based payment method for your customers in Portugal.', + 'woocommerce-payments' + ); + } + + /** + * Returns payment method settings icon. + * + * @param string|null $account_country Country of merchants account. + * @return string + */ + public function get_settings_icon_url( ?string $account_country = null ) { + return plugins_url( 'assets/images/payment-methods/multibanco.svg', WCPAY_PLUGIN_FILE ); + } } diff --git a/includes/payment-methods/class-p24-payment-method.php b/includes/payment-methods/class-p24-payment-method.php index 4237beab155..b538f60ee47 100644 --- a/includes/payment-methods/class-p24-payment-method.php +++ b/includes/payment-methods/class-p24-payment-method.php @@ -42,4 +42,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Accept payments with Przelewy24 (P24), the most popular payment method in Poland.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-sepa-payment-method.php b/includes/payment-methods/class-sepa-payment-method.php index 012828be15c..d60951829f3 100644 --- a/includes/payment-methods/class-sepa-payment-method.php +++ b/includes/payment-methods/class-sepa-payment-method.php @@ -46,4 +46,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return __( 'Test mode: use the test account number AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Reach 500 million customers and over 20 million businesses across the European Union.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-sofort-payment-method.php b/includes/payment-methods/class-sofort-payment-method.php index 291ff6c2b95..edfddc93b1b 100644 --- a/includes/payment-methods/class-sofort-payment-method.php +++ b/includes/payment-methods/class-sofort-payment-method.php @@ -43,4 +43,18 @@ public function __construct( $token_service ) { public function get_testing_instructions( string $account_country ) { return ''; } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.', + 'woocommerce-payments' + ); + } } diff --git a/includes/payment-methods/class-upe-payment-method.php b/includes/payment-methods/class-upe-payment-method.php index 005f8701d6e..b048003707a 100644 --- a/includes/payment-methods/class-upe-payment-method.php +++ b/includes/payment-methods/class-upe-payment-method.php @@ -124,7 +124,7 @@ public function __construct( $token_service, ?string $definition = null ) { if ( null !== $this->definition ) { // Cache values that don't require context. - $this->stripe_id = $this->definition::get_stripe_id(); + $this->stripe_id = $this->definition::get_id(); $this->is_reusable = $this->definition::is_reusable(); $this->currencies = $this->definition::get_supported_currencies(); $this->accept_only_domestic_payment = $this->definition::accepts_only_domestic_payments(); @@ -367,6 +367,91 @@ public function get_countries() { return $this->has_domestic_transactions_restrictions() ? [ $account_country ] : $this->countries; } + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + if ( null !== $this->definition ) { + return $this->definition::get_description( $account_country ); + } + return ''; + } + + /** + * Returns payment method settings label. + * + * @param string $account_country Country of merchants account. + * @return string + */ + public function get_settings_label( string $account_country ) { + if ( null !== $this->definition ) { + return $this->definition::get_settings_label( $account_country ); + } + return $this->get_title( $account_country ); + } + + /** + * Returns payment method settings icon. + * + * @param string|null $account_country Country of merchants account. + * @return string + */ + public function get_settings_icon_url( ?string $account_country = null ) { + if ( null !== $this->definition ) { + return $this->definition::get_settings_icon_url( $account_country ); + } + return $this->get_icon( $account_country ); + } + + /** + * Returns boolean dependent on whether payment method allows manual capture. + * + * @return bool + */ + public function allows_manual_capture() { + if ( null !== $this->definition ) { + return $this->definition::allows_manual_capture(); + } + + return false; + } + + /** + * Returns the Stripe key for the payment method. + * + * @return string + */ + public function get_stripe_key() { + if ( null !== $this->definition ) { + return $this->definition::get_stripe_id(); + } + return \WC_Payments::get_gateway()->get_payment_method_capability_key_map()[ $this->stripe_id ]; + } + + /** + * Returns payment method settings definition. + * + * @param string $account_country Country of merchants account. + * @return array + */ + public function get_payment_method_information_object( string $account_country ) { + return [ + 'id' => $this->get_id(), + 'label' => $this->get_settings_label( $account_country ), + 'description' => $this->get_description( $account_country ), + 'settings_icon_url' => $this->get_settings_icon_url( $account_country ), + 'currencies' => $this->get_currencies(), + 'stripe_key' => $this->get_stripe_key(), + 'allows_manual_capture' => $this->allows_manual_capture(), + 'allows_pay_later' => $this->is_bnpl(), + 'accepts_only_domestic_payment' => $this->has_domestic_transactions_restrictions(), + ]; + } + /** * Returns valid currency to use to filter payment methods. * diff --git a/includes/payment-methods/class-wechatpay-payment-method.php b/includes/payment-methods/class-wechatpay-payment-method.php index de85b8f4829..326ca3b4b5c 100644 --- a/includes/payment-methods/class-wechatpay-payment-method.php +++ b/includes/payment-methods/class-wechatpay-payment-method.php @@ -31,7 +31,6 @@ public function __construct( $token_service ) { $this->icon_url = plugins_url( 'assets/images/payment-methods/wechat-pay.svg', WCPAY_PLUGIN_FILE ); $this->currencies = [ Currency_Code::UNITED_STATES_DOLLAR, - Currency_Code::CHINESE_YUAN, Currency_Code::AUSTRALIAN_DOLLAR, Currency_Code::CANADIAN_DOLLAR, Currency_Code::EURO, @@ -47,7 +46,6 @@ public function __construct( $token_service ) { $this->accept_only_domestic_payment = false; $this->countries = [ Country_Code::UNITED_STATES, - Country_Code::CHINA, Country_Code::AUSTRALIA, Country_Code::CANADA, Country_Code::AUSTRIA, @@ -106,31 +104,29 @@ public function get_currencies() { // Map countries to their primary currencies. switch ( $account_country ) { case Country_Code::AUSTRALIA: - return [ Currency_Code::AUSTRALIAN_DOLLAR, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::AUSTRALIAN_DOLLAR ]; case Country_Code::CANADA: - return [ Currency_Code::CANADIAN_DOLLAR, Currency_Code::CHINESE_YUAN ]; - case Country_Code::CHINA: - return [ Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::CANADIAN_DOLLAR ]; case Country_Code::DENMARK: - return [ Currency_Code::DANISH_KRONE, Currency_Code::EURO, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::DANISH_KRONE ]; case Country_Code::HONG_KONG: - return [ Currency_Code::HONG_KONG_DOLLAR, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::HONG_KONG_DOLLAR ]; case Country_Code::JAPAN: - return [ Currency_Code::JAPANESE_YEN, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::JAPANESE_YEN ]; case Country_Code::NORWAY: - return [ Currency_Code::NORWEGIAN_KRONE, Currency_Code::EURO, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::NORWEGIAN_KRONE ]; case Country_Code::SINGAPORE: - return [ Currency_Code::SINGAPORE_DOLLAR, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::SINGAPORE_DOLLAR ]; case Country_Code::SWEDEN: - return [ Currency_Code::SWEDISH_KRONA, Currency_Code::EURO, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::SWEDISH_KRONA ]; case Country_Code::SWITZERLAND: - return [ Currency_Code::SWISS_FRANC, Currency_Code::EURO, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::SWISS_FRANC ]; case Country_Code::UNITED_KINGDOM: - return [ Currency_Code::POUND_STERLING, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::POUND_STERLING ]; case Country_Code::UNITED_STATES: - return [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::UNITED_STATES_DOLLAR ]; default: - // For all European countries in the supported list, return EUR and CNY. + // For all European countries in the supported list, return EUR. if ( in_array( $account_country, [ @@ -148,11 +144,25 @@ public function get_currencies() { ], true ) ) { - return [ Currency_Code::EURO, Currency_Code::CHINESE_YUAN ]; + return [ Currency_Code::EURO ]; } - // Default to Chinese Yuan. - return [ Currency_Code::CHINESE_YUAN ]; + // Defaulted to unsupported currency. + return [ 'UNSUPPORTED' ]; } } + + /** + * Returns payment method description for the settings page. + * + * @param string|null $account_country Country of merchants account. + * + * @return string + */ + public function get_description( ?string $account_country = null ) { + return __( + 'A digital wallet popular with customers from China.', + 'woocommerce-payments' + ); + } } diff --git a/includes/subscriptions/class-wc-payments-invoice-service.php b/includes/subscriptions/class-wc-payments-invoice-service.php index f8e31323e41..1dd5a36f732 100644 --- a/includes/subscriptions/class-wc-payments-invoice-service.php +++ b/includes/subscriptions/class-wc-payments-invoice-service.php @@ -59,7 +59,7 @@ class WC_Payments_Invoice_Service { * * @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client. * @param WC_Payments_Product_Service $product_service Product Service. - * @param WC_Payments_Order_Service $order_service WC payments Order Service. + * @param WC_Payments_Order_Service $order_service WC payments Order Service. */ public function __construct( WC_Payments_API_Client $payments_api_client, @@ -376,6 +376,30 @@ public function update_charge_details( array $invoice, int $order_id ) { ); } + /** + * Get recurring items of passed item (subscription). + * + * @param WC_Subscription $item Subscription to get recurring items for. + * + * @return array + */ + public function get_recurring_items( $item ) { + // Subscription service has this service as a dependency, so we can't inject it via constructor. + // With this we can mock this function in tests to return whatever we want. + return WC_Payments_Subscriptions::get_subscription_service()->get_recurring_item_data_for_subscription( $item ); + } + + /** + * Get the WCPay subscription item ID for a WC subscription item. + * + * @param WC_Order_Item $item The WC subscription item. + * + * @return string The WCPay subscription item ID. + */ + public function get_wcpay_item_id( $item ) { + return WC_Payments_Subscription_Service::get_wcpay_subscription_item_id( $item ); + } + /** * Sets the subscription last invoice ID meta for WC subscription. * @@ -415,10 +439,11 @@ private function get_repair_data_for_wcpay_items( array $wcpay_item_data, WC_Sub } // Generate any repair data necessary to update the WCPay Subscription so it matches the WC subscription. - foreach ( WC_Payments_Subscriptions::get_subscription_service()->get_recurring_item_data_for_subscription( $subscription ) as $recurring_item_data ) { + $recurring_items = $this->get_recurring_items( $subscription ); + foreach ( $recurring_items as $recurring_item_data ) { $item_id = $recurring_item_data['metadata']['wc_item_id']; $item = $subscription_items[ $item_id ]; - $wcpay_item_id = WC_Payments_Subscription_Service::get_wcpay_subscription_item_id( $item ); + $wcpay_item_id = $this->get_wcpay_item_id( $item ); if ( ! isset( $wcpay_items[ $wcpay_item_id ] ) ) { $message = __( 'The WCPay invoice items do not match WC subscription items.', 'woocommerce-payments' ); diff --git a/includes/subscriptions/class-wc-payments-product-service.php b/includes/subscriptions/class-wc-payments-product-service.php index 6b743bae161..943e331f32e 100644 --- a/includes/subscriptions/class-wc-payments-product-service.php +++ b/includes/subscriptions/class-wc-payments-product-service.php @@ -66,6 +66,13 @@ class WC_Payments_Product_Service { */ private $payments_api_client; + /** + * Account service. + * + * @var WC_Payments_Account + */ + private $account; + /** * The list of products we need to update at the end of each request. * @@ -73,13 +80,22 @@ class WC_Payments_Product_Service { */ private $products_to_update = []; + /** + * The Stripe account ID. + * + * @var string + */ + private $stripe_account_id = null; + /** * Constructor. * * @param WC_Payments_API_Client $payments_api_client Payments API client. + * @param WC_Payments_Account $account Account service. */ - public function __construct( WC_Payments_API_Client $payments_api_client ) { + public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account ) { $this->payments_api_client = $payments_api_client; + $this->account = $account; /** * When a store is in staging mode, we don't want any product handling to be sent to the server. @@ -115,40 +131,86 @@ public static function get_wcpay_product_hash( WC_Product $product ): string { } /** - * Gets the WC Pay product ID associated with a WC product. + * Get or create the WC Pay product ID associated with a WC product. * - * @param WC_Product $product The product to get the WC Pay ID for. + * @param WC_Product $product The product to get the WC Pay ID for. * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The WC Pay product ID or an empty string. + * @throws Exception */ - public function get_wcpay_product_id( WC_Product $product, $test_mode = null ): string { + public function get_or_create_wcpay_product_id( WC_Product $product, $test_mode = null ): string { // If the subscription product doesn't have a WC Pay product ID, create one. - if ( ! self::has_wcpay_product_id( $product, $test_mode ) ) { + if ( ! $this->has_wcpay_product_id( $product, $test_mode ) ) { $is_current_environment = null === $test_mode || WC_Payments::mode()->is_test() === $test_mode; // Only create a new wcpay product if we're trying to fetch a wcpay product ID in the current environment. if ( $is_current_environment ) { - WC_Payments_Subscriptions::get_product_service()->create_product( $product ); + $this->create_product( $product ); } } - return $product->get_meta( self::get_wcpay_product_id_option( $test_mode ), true ); + return $product->get_meta( self::get_wcpay_product_id_meta_key( $test_mode ), true ); } /** - * Gets the WC Pay product ID associated with a WC product. + * Get the WCPay product ID for an item type. * - * @param string $type The item type to create a product for. - * @return string The item's WCPay product id. + * @param string $type The item type. + * + * @return string The WCPay product ID. + * @throws API_Exception */ public function get_wcpay_product_id_for_item( string $type ): string { - $sanitized_type = self::sanitize_option_key( $type ); - $option_key_name = self::get_wcpay_product_id_option() . '_' . $sanitized_type; - if ( ! get_option( $option_key_name ) ) { - $this->create_product_for_item_type( $sanitized_type ); + $sanitized_type = self::sanitize_option_key( $type ); + $option_key_name = self::get_wcpay_product_id_option( $sanitized_type ); + $wcpay_product_id = get_option( $option_key_name ); + + // Case 1: No product found, create a new one. + if ( ! $wcpay_product_id ) { + return $this->create_product_for_item_type( $sanitized_type ); } - return get_option( $option_key_name ); + + // For existing products, check the linked account. + $linked_option_key = self::get_wcpay_product_id_linked_to_key( $sanitized_type ); + $linked_account_id = get_option( $linked_option_key ); + $stripe_account_id = $this->account->get_stripe_account_id(); + + // Case 2: Product exists but linked account doesn't, validate and update if needed. + if ( ! $linked_account_id ) { + try { + // Validate that the product exists for the current account. + $existing_product = $this->payments_api_client->get_product_by_id( $wcpay_product_id ); + + if ( $existing_product ) { + // Product exists, save with current account ID. + $this->save_wcpay_product_data( $wcpay_product_id, $stripe_account_id, $sanitized_type ); + return $wcpay_product_id; + } else { + // Product doesn't exist, create new one. + return $this->create_product_for_item_type( $sanitized_type ); + } + } catch ( \Exception $e ) { + // Error occurred, create new product. + Logger::log( + sprintf( + 'Error occurred when fetching product : wcpay_product_id=%s, account_id=%s, error=%s', + $wcpay_product_id, + $stripe_account_id, + $e->getMessage() + ) + ); + return $this->create_product_for_item_type( $sanitized_type ); + } + } + + // Case 3: Product exists but for a different Stripe account, create new one. + if ( $linked_account_id !== $stripe_account_id ) { + return $this->create_product_for_item_type( $sanitized_type ); + } + + // Case 4: Valid product exists for current account. + return $wcpay_product_id; } /** @@ -162,15 +224,78 @@ public static function sanitize_option_key( string $type ) { } /** - * Check if the WC product has a WC Pay product ID. + * Save wcpay product data across two related options. + * + * @param string $wcpay_product_id The WooCommerce Payments product ID. + * @param string $stripe_account_id The Stripe account identifier. + * @param string $type The item type used to construct the option key. + * + * @return void + */ + private function save_wcpay_product_data( string $wcpay_product_id, string $stripe_account_id, string $type ) { + $sanitized_type = self::sanitize_option_key( $type ); + $option_key_name = self::get_wcpay_product_id_option( $sanitized_type ); + + // Store product ID. + update_option( $option_key_name, $wcpay_product_id ); + + // Store linked stripe account ID. + $linked_option_key = self::get_wcpay_product_id_linked_to_key( $sanitized_type ); + update_option( $linked_option_key, $stripe_account_id ); + } + + /** + * Check if the WC product has a valid WC Pay product ID linked to the current Stripe account. * * @param WC_Product $product The product to get the WC Pay ID for. * @param bool|null $test_mode Is WC Pay in test/dev mode. * - * @return bool The WC Pay product ID or an empty string. + * @return bool Whether the product has a valid WCPay product ID. */ - public static function has_wcpay_product_id( WC_Product $product, $test_mode = null ): bool { - return (bool) $product->get_meta( self::get_wcpay_product_id_option( $test_mode ) ); + public function has_wcpay_product_id( WC_Product $product, $test_mode = null ): bool { + $option_key = self::get_wcpay_product_id_meta_key( $test_mode ); + $wcpay_product_id = $product->get_meta( $option_key ); + + // No product ID exists. + if ( empty( $wcpay_product_id ) ) { + return false; + } + + // Check if we have the linked account metadata. + $linked_option_key = self::get_wcpay_product_id_linked_to_key( null, $test_mode ); + $linked_account_id = $product->get_meta( $linked_option_key ); + $current_account_id = $this->account->get_stripe_account_id(); + + // If we have linked account metadata, just compare with current account. + if ( ! empty( $linked_account_id ) ) { + return $linked_account_id === $current_account_id; + } + + // Legacy case: we have a product ID but no linked account. + // Verify if product exists for current account. + try { + $product_data = $this->payments_api_client->get_product_by_id( $wcpay_product_id ); + + // Product exists, update metadata with current account. + if ( ! empty( $product_data ) ) { + $product->update_meta_data( $linked_option_key, $current_account_id ); + $product->save(); + return true; + } + + return false; + } catch ( \Exception $e ) { + Logger::log( + sprintf( + 'Error validating WCPay product: product_id=%d, wcpay_product_id=%s, account_id=%s, error=%s', + $product->get_id(), + $wcpay_product_id, + $current_account_id, + $e->getMessage() + ) + ); + return false; + } } /** @@ -222,7 +347,7 @@ public function maybe_schedule_product_create_or_update( int $product_id ) { continue; } - if ( ! self::has_wcpay_product_id( $product_to_update ) || $this->product_needs_update( $product_to_update ) ) { + if ( ! $this->has_wcpay_product_id( $product_to_update ) || $this->product_needs_update( $product_to_update ) ) { $this->products_to_update[ $product_to_update->get_id() ] = $product_to_update->get_id(); } } @@ -254,7 +379,8 @@ public function create_or_update_products() { */ public function create_product( WC_Product $product ) { try { - $product_data = $this->get_product_data( $product ); + $product_data = $this->get_product_data( $product ); + $stripe_account_id = $this->account->get_stripe_account_id(); // Validate that we have enough data to create the product. $this->validate_product_data( $product_data ); @@ -263,7 +389,7 @@ public function create_product( WC_Product $product ) { $this->remove_product_update_listeners(); $this->set_wcpay_product_hash( $product, $this->get_product_hash( $product ) ); - $this->set_wcpay_product_id( $product, $wcpay_product['wcpay_product_id'] ); + $this->set_wcpay_product_id( $product, $wcpay_product['wcpay_product_id'], $stripe_account_id ); $this->add_product_update_listeners(); } catch ( \Exception $e ) { Logger::log( sprintf( 'There was a problem creating the product #%s in WC Pay: %s', $product->get_id(), $e->getMessage() ) ); @@ -274,20 +400,21 @@ public function create_product( WC_Product $product ) { * Create a generic item product in WC Pay. * * @param string $type The item type to create a product for. + * + * @return string The created WCPay product ID. + * @throws API_Exception */ - public function create_product_for_item_type( string $type ) { - try { - $wcpay_product = $this->payments_api_client->create_product( - [ - 'description' => 'N/A', - 'name' => ucfirst( $type ), - ] - ); + private function create_product_for_item_type( string $type ): string { + $wcpay_product = $this->payments_api_client->create_product( + [ + 'description' => 'N/A', + 'name' => ucfirst( $type ), + ] + ); + $stripe_account_id = $this->account->get_stripe_account_id(); + $this->save_wcpay_product_data( $wcpay_product['wcpay_product_id'], $stripe_account_id, $type ); - update_option( self::get_wcpay_product_id_option() . '_' . $type, $wcpay_product['wcpay_product_id'] ); - } catch ( API_Exception $e ) { - Logger::log( 'There was a problem creating the product on WCPay Server: ' . $e->getMessage() ); - } + return $wcpay_product['wcpay_product_id']; } /** @@ -312,7 +439,6 @@ public function update_products( WC_Product $product ) { if ( empty( $wcpay_product_ids ) ) { return; } - if ( ! $this->product_needs_update( $product ) ) { return; } @@ -623,24 +749,59 @@ private function set_wcpay_product_hash( WC_Product $product, string $value ) { } /** - * Sets a WC Pay product ID on a WC product. + * Sets a WC Pay product ID and the Stripe account it's linked to on a WC product. * - * @param WC_Product $product The product to set the WC Pay ID for. - * @param string $value The WC Pay product ID. + * @param WC_Product $product The product to set the WC Pay ID for. + * @param string $wcpay_product_id The WC Pay product ID. + * @param string $stripe_account_id The Stripe account ID. */ - private function set_wcpay_product_id( WC_Product $product, string $value ) { - $product->update_meta_data( self::get_wcpay_product_id_option(), $value ); + private function set_wcpay_product_id( WC_Product $product, string $wcpay_product_id, string $stripe_account_id ) { + $option_key = self::get_wcpay_product_id_meta_key(); + $link_key = self::get_wcpay_product_id_linked_to_key(); + $product->update_meta_data( $option_key, $wcpay_product_id ); + $product->update_meta_data( $link_key, $stripe_account_id ); $product->save(); } /** * Returns the name of the product id option meta, taking test mode into account. * - * @param bool|null $test_mode Is WC Pay in test/dev mode. + * @param string|null $type The item type. + * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The WCPay product ID meta key/option name. */ - public static function get_wcpay_product_id_option( $test_mode = null ): string { + public static function get_wcpay_product_id_option( ?string $type = null, ?bool $test_mode = null ): string { + $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; + $key = $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY; + return $type ? $key . '_' . $type : $key; + } + + /** + * Returns the name of the product id linked to account option meta, taking test mode into account. + * + * @param string|null $type The item type. + * @param bool|null $test_mode Is WC Pay in test/dev mode. + * + * @return string The WCPay product ID meta key/option name. + */ + public static function get_wcpay_product_id_linked_to_key( ?string $type = null, ?bool $test_mode = null ): string { + $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; + $key = $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY; + return ( $type ? $key . '_' . $type : $key ) . '_linked_to'; + } + + /** + * Returns the name of the wcpay product id meta key. + * + * @param bool|null $test_mode Is WCPay in test, prod or dev mode. + * + * @return string The product id meta key. + * @throws Exception + */ + public static function get_wcpay_product_id_meta_key( $test_mode = null ): string { + // This functions looks the same as the one above. + // It's here to avoid potential issue when we change the above function. $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; return $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY; } @@ -666,8 +827,8 @@ public static function get_wcpay_price_id_option( $test_mode = null ): string { */ private function get_all_wcpay_product_ids( WC_Product $product ) { $environment_product_ids = [ - 'live' => self::has_wcpay_product_id( $product, false ) ? $this->get_wcpay_product_id( $product, false ) : null, - 'test' => self::has_wcpay_product_id( $product, true ) ? $this->get_wcpay_product_id( $product, true ) : null, + 'live' => $this->has_wcpay_product_id( $product, false ) ? $this->get_or_create_wcpay_product_id( $product, false ) : null, + 'test' => $this->has_wcpay_product_id( $product, true ) ? $this->get_or_create_wcpay_product_id( $product, true ) : null, ]; return array_filter( $environment_product_ids ); @@ -724,6 +885,7 @@ private function delete_all_wcpay_price_ids( $product ) { // Now that the price has been archived, delete the record of it. $product->delete_meta_data( $price_id_meta_key ); + $product->delete_meta_data( $price_id_meta_key . '_linked_to' ); } } @@ -803,7 +965,7 @@ public function get_wcpay_price_id( WC_Product $product, $test_mode = null ): st // Only create WCPay Price object if we're trying to getch a wcpay price ID in the current environment. if ( $is_current_environment ) { - WC_Payments_Subscriptions::get_product_service()->create_product( $product ); + $this->create_product( $product ); $price_id = $product->get_meta( self::get_wcpay_price_id_option(), true ); } } diff --git a/includes/subscriptions/class-wc-payments-subscription-service.php b/includes/subscriptions/class-wc-payments-subscription-service.php index 18ddd8caff9..9f63d247d5b 100644 --- a/includes/subscriptions/class-wc-payments-subscription-service.php +++ b/includes/subscriptions/class-wc-payments-subscription-service.php @@ -5,9 +5,11 @@ * @package WooCommerce\Payments */ +use WCPay\Constants\Order_Mode; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\Cannot_Combine_Currencies_Exception; +use WCPay\Exceptions\Subscription_Mode_Mismatch_Exception; use WCPay\Logger; /** @@ -162,6 +164,8 @@ public function __construct( add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'show_wcpay_subscription_id' ] ); add_action( 'woocommerce_subscription_payment_method_updated_from_' . WC_Payment_Gateway_WCPay::GATEWAY_ID, [ $this, 'maybe_cancel_subscription' ], 10, 2 ); + + add_action( 'wcs_renewal_order_items', [ $this, 'check_wcpay_mode_for_subscription' ], 10, 3 ); } } @@ -823,7 +827,7 @@ public function get_recurring_item_data_for_subscription( WC_Subscription $subsc $data[] = [ 'metadata' => $this->get_item_metadata( $item ), 'quantity' => $item->get_quantity(), - 'price_data' => static::format_item_price_data( $subscription->get_currency(), $this->product_service->get_wcpay_product_id( $item->get_product() ), $item->get_subtotal() / $item->get_quantity(), $subscription->get_billing_period(), $subscription->get_billing_interval() ), + 'price_data' => static::format_item_price_data( $subscription->get_currency(), $this->product_service->get_or_create_wcpay_product_id( $item->get_product() ), $item->get_subtotal() / $item->get_quantity(), $subscription->get_billing_period(), $subscription->get_billing_interval() ), ]; } @@ -874,6 +878,36 @@ public function maybe_cancel_subscription( $subscription, $new_payment_method ) } } + /** + * Checks if the original subscription mode matches current WooPayments mode. + * + * If the original subscription was payed with WooPayments, but in the mode, that doesn't + * match the current WooPayments mode, we need to throw an exception, to prevent the renewal + * order from being created, as it would fail to be paid. + * + * @param array $items The items to be added to the renewal order. + * @param WC_Order $order Renewal order. + * @param WC_Subscription $subscription The original subscription. + * @throws Subscription_Mode_Mismatch_Exception + * @return array + */ + public function check_wcpay_mode_for_subscription( array $items, WC_Order $order, WC_Subscription $subscription ): array { + $parent_order = $subscription->get_parent(); + if ( false !== $parent_order ) { + $subscription_mode = $parent_order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY ); + $current_mode = WC_Payments::mode()->is_test() ? Order_Mode::TEST : Order_Mode::PRODUCTION; + + if ( is_string( $subscription_mode ) && '' !== $subscription_mode && $subscription_mode !== $current_mode ) { + if ( Order_Mode::TEST === $subscription_mode ) { + throw new Subscription_Mode_Mismatch_Exception( __( 'Subscription was made when WooPayments was in the test mode and cannot be renewed in the live mode.', 'woocommerce-payments' ) ); + } else { + throw new Subscription_Mode_Mismatch_Exception( __( 'Subscription was made when WooPayments was in the live mode and cannot be renewed in the test mode.', 'woocommerce-payments' ) ); + } + } + } + return $items; + } + /** * Gets one time item data from a subscription needed to create a WCPay subscription. * diff --git a/includes/subscriptions/class-wc-payments-subscriptions.php b/includes/subscriptions/class-wc-payments-subscriptions.php index e26020cb335..b3c86b1e08a 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions.php +++ b/includes/subscriptions/class-wc-payments-subscriptions.php @@ -81,7 +81,7 @@ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Cus include_once __DIR__ . '/class-wc-payments-subscription-minimum-amount-handler.php'; // Instantiate additional classes. - self::$product_service = new WC_Payments_Product_Service( $api_client ); + self::$product_service = new WC_Payments_Product_Service( $api_client, $account ); self::$invoice_service = new WC_Payments_Invoice_Service( $api_client, self::$product_service, self::$order_service ); self::$subscription_service = new WC_Payments_Subscription_Service( $api_client, $customer_service, self::$product_service, self::$invoice_service ); self::$event_handler = new WC_Payments_Subscriptions_Event_Handler( self::$invoice_service, self::$subscription_service ); diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 0dc37ef7482..6a767b4cac6 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -1038,19 +1038,30 @@ public function request_capability( string $capability_id, bool $requested ) { /** * Get data needed to initialize the onboarding flow * - * @param bool $live_account Whether to get the onboarding data for a live mode or test mode account. - * @param string $return_url URL to redirect to at the end of the flow. - * @param array $site_data Data to track ToS agreement. - * @param array $user_data Data about the user doing the onboarding (location and device). - * @param array $account_data Data to prefill the onboarding. - * @param array $actioned_notes Actioned WCPay note names to be sent to the onboarding flow. - * @param bool $progressive Whether we need to enable progressive onboarding prefill. - * @param bool $collect_payout_requirements Whether we need to redirect user to Stripe KYC to complete their payouts data. + * @param bool $live_account Whether to get the onboarding data for a live mode or test mode account. + * @param string $return_url URL to redirect to at the end of the flow. + * @param array $site_data Data to track ToS agreement. + * @param array $user_data Data about the user doing the onboarding (location and device). + * @param array $account_data Data to prefill the onboarding. + * @param array $actioned_notes Actioned WCPay note names to be sent to the onboarding flow. + * @param bool $progressive Whether we need to enable progressive onboarding prefill. + * @param bool $collect_payout_requirements Whether we need to redirect user to Stripe KYC to complete their payouts data. + * @param ?string $referral_code Referral code to be used for onboarding. * * @return array An array containing the url and state fields. * @throws API_Exception Exception thrown on request failure. */ - public function get_onboarding_data( bool $live_account, string $return_url, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $progressive = false, bool $collect_payout_requirements = false ): array { + public function get_onboarding_data( + bool $live_account, + string $return_url, + array $site_data = [], + array $user_data = [], + array $account_data = [], + array $actioned_notes = [], + bool $progressive = false, + bool $collect_payout_requirements = false, + ?string $referral_code = null + ): array { $request_args = apply_filters( 'wc_payments_get_onboarding_data_args', [ @@ -1065,24 +1076,35 @@ public function get_onboarding_data( bool $live_account, string $return_url, arr ] ); + $request_args['referral_code'] = $referral_code; + return $this->request( $request_args, self::ONBOARDING_API . '/init', self::POST, true, true ); } /** * Initialize the onboarding embedded KYC flow, returning a session object which is used by the frontend. * - * @param bool $live_account Whether to create live account. - * @param array $site_data Site data. - * @param array $user_data User data. - * @param array $account_data Account data to be prefilled. - * @param array $actioned_notes Actioned notes to be sent. - * @param bool $progressive Whether progressive onboarding should be enabled for this onboarding. + * @param bool $live_account Whether to create live account. + * @param array $site_data Site data. + * @param array $user_data User data. + * @param array $account_data Account data to be prefilled. + * @param array $actioned_notes Actioned notes to be sent. + * @param bool $progressive Whether progressive onboarding should be enabled for this onboarding. + * @param ?string $referral_code Referral code to be used for onboarding. * * @return array * * @throws API_Exception */ - public function initialize_onboarding_embedded_kyc( bool $live_account, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $progressive = false ): array { + public function initialize_onboarding_embedded_kyc( + bool $live_account, + array $site_data = [], + array $user_data = [], + array $account_data = [], + array $actioned_notes = [], + bool $progressive = false, + ?string $referral_code = null + ): array { $request_args = apply_filters( 'wc_payments_get_onboarding_data_args', [ @@ -1095,6 +1117,8 @@ public function initialize_onboarding_embedded_kyc( bool $live_account, array $s ] ); + $request_args['referral_code'] = $referral_code; + $session = $this->request( $request_args, self::ONBOARDING_API . '/embedded', self::POST, true, true ); if ( ! is_array( $session ) ) { @@ -1273,6 +1297,23 @@ public function update_customer( $customer_id, $customer_data = [] ) { ); } + /** + * Fetch a product. + * + * @param string $product_id ID of the product to get. + * + * @return array The product. + * + * @throws API_Exception If fetching the product fails. + */ + public function get_product_by_id( string $product_id ): array { + return $this->request( + [], + self::PRODUCTS_API . '/' . $product_id, + self::GET + ); + } + /** * Create a product. * @@ -2254,25 +2295,6 @@ protected function request( $params, $api, $method, $is_site_specific = true, $u $this->check_response_for_errors( $response ); } catch ( Connection_Exception $e ) { $last_exception = $e; - } catch ( API_Exception $e ) { - if ( isset( $params['level3'] ) && 'invalid_request_error' === $e->get_error_code() ) { - // phpcs:disable WordPress.PHP.DevelopmentFunctions - - // Log the issue so we could debug it. - Logger::error( - 'Level3 data error: ' . PHP_EOL - . print_r( $e->getMessage(), true ) . PHP_EOL - . print_r( 'Level 3 data sent: ', true ) . PHP_EOL - . print_r( $params['level3'], true ) - ); - - // phpcs:enable WordPress.PHP.DevelopmentFunctions - - // Retry without level3 data. - unset( $params['level3'] ); - return $this->request( $params, $api, $method, $is_site_specific, $use_user_token, $raw_response ); - } - throw $e; } if ( $response_code || time() >= $stop_trying_at || $retries_limit === $retries ) { @@ -2304,44 +2326,6 @@ protected function request( $params, $api, $method, $is_site_specific = true, $u return $response_body; } - /** - * Handles issues with level3 data and retries requests when necessary. - * - * @param array $params - Request parameters to send as either JSON or GET string. Defaults to test_mode=1 if either in dev or test mode, 0 otherwise. - * @param string $api - The API endpoint to call. - * @param string $method - The HTTP method to make the request with. - * @param bool $is_site_specific - If true, the site ID will be included in the request url. - * - * @return array - * @throws API_Exception - If the account ID hasn't been set. - */ - private function request_with_level3_data( $params, $api, $method, $is_site_specific = true ) { - // If level3 data is not present for some reason, simply proceed normally. - if ( empty( $params['level3'] ) || ! is_array( $params['level3'] ) ) { - return $this->request( $params, $api, $method, $is_site_specific ); - } - - // If level3 data doesn't contain any items, add a zero priced fee to meet Stripe's requirement. - if ( ! isset( $params['level3']['line_items'] ) || ! is_array( $params['level3']['line_items'] ) || 0 === count( $params['level3']['line_items'] ) ) { - $params['level3']['line_items'] = [ - [ - 'discount_amount' => 0, - 'product_code' => 'empty-order', - 'product_description' => 'The order is empty', - 'quantity' => 1, - 'tax_amount' => 0, - 'unit_cost' => 0, - ], - ]; - } - - /** - * In case of invalid request errors, level3 data is now removed, - * and the request is retried within `request()` instead of here. - */ - return $this->request( $params, $api, $method, $is_site_specific ); - } - /** * From a given response extract the body. * diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php index 73083341975..d8832711482 100644 --- a/includes/woopay/class-woopay-utilities.php +++ b/includes/woopay/class-woopay-utilities.php @@ -148,6 +148,16 @@ public function is_country_available() { return in_array( $location_data['country'], $available_countries, true ); } + /** + * Sanitizes an intent ID by stripping everything by underscores, characters and digits. + * + * @param string $intent_id ID of the intent. + * @return string Sanitized value. + */ + public static function sanitize_intent_id( string $intent_id ) { + return preg_replace( '/[^\w_]+/', '', $intent_id ); + } + /** * Get if WooPay is available on the store country. * diff --git a/package-lock.json b/package-lock.json index ee2f86d529e..773c34dbc6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "woocommerce-payments", - "version": "9.1.0", + "version": "9.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "woocommerce-payments", - "version": "9.1.0", + "version": "9.2.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { @@ -74,7 +74,7 @@ "@wordpress/html-entities": "3.6.1", "@wordpress/i18n": "4.6.1", "@wordpress/icons": "10.19.0", - "@wordpress/jest-preset-default": "8.1.2", + "@wordpress/jest-preset-default": "12.21.0", "@wordpress/plugins": "4.4.3", "@wordpress/primitives": "3.30.0", "@wordpress/scripts": "28.3.0", @@ -106,6 +106,7 @@ "iti": "0.6.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", + "jquery": "3.7.1", "lint-staged": "10.5.4", "mini-css-extract-plugin": "2.3.0", "moment": "2.29.4", @@ -4236,32 +4237,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/expect/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -4274,54 +4249,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/expect/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/expect/node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", @@ -4373,19 +4300,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "node_modules/@jest/expect/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/@jest/fake-timers": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", @@ -4446,154 +4360,42 @@ } }, "node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/@jest/transform/node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/@jest/transform/node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "write-file-atomic": "^4.0.2" }, "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/@jest/transform/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@jest/types": { @@ -6185,15 +5987,6 @@ "integrity": "sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==", "dev": true }, - "node_modules/@types/cheerio": { - "version": "0.22.35", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", - "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -7177,55 +6970,6 @@ } } }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.7.tgz", - "integrity": "sha512-B+byiwi/T1bx5hcj9wc0fUL5Hlb5giSXJzcnEfJVl2j6dGV2NJfcxDBYX0WWwIxlzNiFz8kAvlkFWI2y/nscZQ==", - "dev": true, - "dependencies": { - "@wojtekmaj/enzyme-adapter-utils": "^0.1.4", - "enzyme-shallow-equal": "^1.0.0", - "has": "^1.0.0", - "prop-types": "^15.7.0", - "react-is": "^17.0.0", - "react-test-renderer": "^17.0.0" - }, - "peerDependencies": { - "enzyme": "^3.0.0", - "react": "^17.0.0-0", - "react-dom": "^17.0.0-0" - } - }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/react-test-renderer": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", - "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^17.0.2", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/@wojtekmaj/enzyme-adapter-utils": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.4.tgz", - "integrity": "sha512-ARGIQSIIv3oBia1m5Ihn1VU0FGmft6KPe39SBKTb8p7LSXO23YI4kNtc4M/cKoIY7P+IYdrZcgMObvedyjoSQA==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.0", - "has": "^1.0.0", - "object.fromentries": "^2.0.0", - "prop-types": "^15.7.0" - }, - "peerDependencies": { - "react": "^17.0.0-0" - } - }, "node_modules/@woocommerce/api": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@woocommerce/api/-/api-0.2.0.tgz", @@ -12869,41 +12613,38 @@ } }, "node_modules/@wordpress/jest-console": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-5.4.0.tgz", - "integrity": "sha512-Yan361XouPSi/HT30Dv94Srdy5iKk1ayBL+pLGvCiDEyLyB6dpLU2XmXUqDpdCjKAV6+TA1N85voKQNd66ZBLQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.21.0.tgz", + "integrity": "sha512-y5VFn//Tt7sASccwKGHkjLj3yHWSABiutEusTx2PJ1CzOJwTWldf5KDTnMqMciXf2In84ImV9dLyoI1pllJP0g==", "dev": true, "dependencies": { - "@babel/runtime": "^7.16.0", - "jest-matcher-utils": "^27.4.2" + "@babel/runtime": "7.25.7", + "jest-matcher-utils": "^29.6.2" }, "engines": { - "node": ">=12" + "node": ">=18.12.0", + "npm": ">=8.19.2" }, "peerDependencies": { - "jest": ">=27" + "jest": ">=29" } }, "node_modules/@wordpress/jest-preset-default": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-8.1.2.tgz", - "integrity": "sha512-7zoXcBfhX3hS+qY9dZZ+x70uk4AnYMe0+VB28ekMPqZEI2NnIA0u1bsGuHnnf3CfSVBE8hzGIoeDP02QG16XrQ==", + "version": "12.21.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.21.0.tgz", + "integrity": "sha512-heTTSDh1THOa+9TnpPYZ07B7iZVstrPvfB5pYjOrnig3/DeYu1cgQpUJiwkvot6P+weFhhWSZoFXossItXQVTA==", "dev": true, "dependencies": { - "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", - "@wordpress/jest-console": "^5.0.3", - "babel-jest": "^27.4.5", - "enzyme": "^3.11.0", - "enzyme-to-json": "^3.4.4" + "@wordpress/jest-console": "^8.21.0", + "babel-jest": "29.7.0" }, "engines": { - "node": ">=12" + "node": ">=18.12.0", + "npm": ">=8.19.2" }, "peerDependencies": { "@babel/core": ">=7", - "jest": ">=27", - "react": "^17.0.0", - "react-dom": "^17.0.0" + "jest": ">=29" } }, "node_modules/@wordpress/keyboard-shortcuts": { @@ -15316,36 +15057,10 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@wordpress/scripts/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@wordpress/scripts/node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", - "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", + "node_modules/@wordpress/scripts/node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", + "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", "dev": true, "dependencies": { "ansi-html": "^0.0.9", @@ -15460,41 +15175,6 @@ "webpack": "^5.0.0" } }, - "node_modules/@wordpress/scripts/node_modules/@wordpress/jest-console": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.3.0.tgz", - "integrity": "sha512-aPVdS1V5YLlqhc00458qmc1TujhDbi6WDoT9m63aOuRGcmb223DNuKEkI77p0RV3F7QB39dBqa466mOUbKaX2w==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.16.0", - "jest-matcher-utils": "^29.6.2" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" - }, - "peerDependencies": { - "jest": ">=29" - } - }, - "node_modules/@wordpress/scripts/node_modules/@wordpress/jest-preset-default": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.3.0.tgz", - "integrity": "sha512-xYXndYJFr9QDsO3fTl5JUXJs80Xq/fxLiOa4zlrrXmWy3L/7+uPik9Jgy0KZOu4FqiYp5wJFX6Arp9dzqH3Xug==", - "dev": true, - "dependencies": { - "@wordpress/jest-console": "^8.3.0", - "babel-jest": "^29.6.2" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" - }, - "peerDependencies": { - "@babel/core": ">=7", - "jest": ">=29" - } - }, "node_modules/@wordpress/scripts/node_modules/@wordpress/prettier-config": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.3.0.tgz", @@ -15520,45 +15200,12 @@ "node": ">=0.4.0" } }, - "node_modules/@wordpress/scripts/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@wordpress/scripts/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/@wordpress/scripts/node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, "node_modules/@wordpress/scripts/node_modules/babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -15596,37 +15243,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/@wordpress/scripts/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@wordpress/scripts/node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@wordpress/scripts/node_modules/balanced-match": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", @@ -15642,15 +15258,6 @@ "node": ">=14" } }, - "node_modules/@wordpress/scripts/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@wordpress/scripts/node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -15875,45 +15482,6 @@ "node": ">=8" } }, - "node_modules/@wordpress/scripts/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@wordpress/scripts/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@wordpress/scripts/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@wordpress/scripts/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -16057,26 +15625,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@wordpress/scripts/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@wordpress/scripts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@wordpress/scripts/node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -17658,26 +17206,6 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.filter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.4.tgz", - "integrity": "sha512-r+mCJ7zXgXElgR4IRC+fkvNCeoaavWBs6EdCso5Tbcf+iEMKzBU/His60lt34WEZ9vlb8wDkZvQGcVI5GwkfoQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-array-method-boxes-properly": "^1.0.0", - "es-object-atoms": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.find": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.3.tgz", @@ -17967,52 +17495,26 @@ } }, "node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, - "node_modules/babel-jest/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-jest/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/babel-loader": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", @@ -18229,18 +17731,18 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", + "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-plugin-macros": { @@ -18335,16 +17837,16 @@ } }, "node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -19099,44 +18601,6 @@ "semver": "bin/semver.js" } }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "dev": true, - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -20026,115 +19490,28 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-jest/node_modules/@jest/transform": { + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/jest-config": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/create-jest/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-jest/node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/create-jest/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-jest/node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", @@ -20170,21 +19547,6 @@ } } }, - "node_modules/create-jest/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/create-jest/node_modules/jest-docblock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", @@ -20210,30 +19572,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-jest/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-jest/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/create-jest/node_modules/jest-resolve": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", @@ -20480,19 +19818,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/create-jest/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -21533,12 +20858,12 @@ } }, "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/diffie-hellman": { @@ -21582,12 +20907,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -21945,39 +21264,6 @@ "node": ">=4" } }, - "node_modules/enzyme": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", - "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", - "dev": true, - "dependencies": { - "array.prototype.flat": "^1.2.3", - "cheerio": "^1.0.0-rc.3", - "enzyme-shallow-equal": "^1.0.1", - "function.prototype.name": "^1.1.2", - "has": "^1.0.3", - "html-element-map": "^1.2.0", - "is-boolean-object": "^1.0.1", - "is-callable": "^1.1.5", - "is-number-object": "^1.0.4", - "is-regex": "^1.0.5", - "is-string": "^1.0.5", - "is-subset": "^0.1.1", - "lodash.escape": "^4.0.1", - "lodash.isequal": "^4.5.0", - "object-inspect": "^1.7.0", - "object-is": "^1.0.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.1", - "object.values": "^1.1.1", - "raf": "^3.4.1", - "rst-selector-parser": "^2.2.3", - "string.prototype.trim": "^1.2.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/enzyme-shallow-equal": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.7.tgz", @@ -21990,29 +21276,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/enzyme-to-json": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.2.tgz", - "integrity": "sha512-Ynm6Z6R6iwQ0g2g1YToz6DWhxVnt8Dy1ijR2zynRKxTyBGA8rCDXU3rs2Qc4OKvUvc2Qoe1bcFK6bnPs20TrTg==", - "dev": true, - "dependencies": { - "@types/cheerio": "^0.22.22", - "lodash": "^4.17.21", - "react-is": "^16.12.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "enzyme": "^3.4.0" - } - }, - "node_modules/enzyme-to-json/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "node_modules/equivalent-key-map": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/equivalent-key-map/-/equivalent-key-map-0.2.2.tgz", @@ -22099,12 +21362,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -23046,77 +22303,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/expect/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/expect/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -24435,19 +23621,6 @@ "resolved": "https://registry.npmjs.org/hpq/-/hpq-1.4.0.tgz", "integrity": "sha512-ycJQMRaRPBcfnoT1gS5I1XCvbbw9KO94Y0vkwksuOjcJMqNZtb03MF2tCItLI2mQbkZWSSeFinoRDPmjzv4tKg==" }, - "node_modules/html-element-map": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", - "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", - "dev": true, - "dependencies": { - "array.prototype.filter": "^1.0.0", - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -24494,25 +23667,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -25454,12 +24608,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", - "dev": true - }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -25907,32 +25055,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-circus/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -25959,30 +25081,6 @@ } } }, - "node_modules/jest-circus/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-circus/node_modules/jest-each": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", @@ -25999,30 +25097,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-circus/node_modules/jest-resolve": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", @@ -26144,19 +25218,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "node_modules/jest-circus/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -26324,32 +25385,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-cli/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-cli/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -26362,58 +25397,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-cli/node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/jest-cli/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli/node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/jest-cli/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -26428,15 +25411,6 @@ "node": ">=12" } }, - "node_modules/jest-cli/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-cli/node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -26498,21 +25472,6 @@ } } }, - "node_modules/jest-cli/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-cli/node_modules/jest-docblock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", @@ -26538,30 +25497,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-cli/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-cli/node_modules/jest-resolve": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", @@ -26822,19 +25757,6 @@ "node": ">=10.12.0" } }, - "node_modules/jest-cli/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/jest-cli/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -26881,18 +25803,18 @@ } }, "node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-diff/node_modules/ansi-styles": { @@ -26907,29 +25829,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-diff/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/jest-environment-jsdom": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", @@ -27008,15 +25927,6 @@ "fsevents": "^2.3.2" } }, - "node_modules/jest-haste-map/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-haste-map/node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", @@ -27048,18 +25958,18 @@ } }, "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { @@ -27074,29 +25984,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-matcher-utils/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -27164,128 +26071,54 @@ } }, "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/jest-resolve-dependencies/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-resolve-dependencies/node_modules/jest-matcher-utils": { + "node_modules/jest-resolve-dependencies": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "node_modules/jest-resolve-dependencies/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-resolve-dependencies/node_modules/jest-snapshot": { @@ -27339,19 +26172,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "node_modules/jest-resolve-dependencies/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -27503,32 +26323,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -27541,67 +26335,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest/node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/jest/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest/node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/jest/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest/node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -27663,21 +26396,6 @@ } } }, - "node_modules/jest/node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest/node_modules/jest-docblock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", @@ -27703,30 +26421,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest/node_modules/jest-resolve": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", @@ -27987,19 +26681,6 @@ "node": ">=10.12.0" } }, - "node_modules/jest/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -28019,6 +26700,12 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "dev": true }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true + }, "node_modules/js-library-detector": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", @@ -28951,12 +27638,6 @@ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true }, - "node_modules/lodash.escape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", - "dev": true - }, "node_modules/lodash.find": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-3.2.1.tgz", @@ -28976,12 +27657,6 @@ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -28992,12 +27667,6 @@ "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==" }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -29839,12 +28508,6 @@ "node": "*" } }, - "node_modules/moo": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", - "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "dev": true - }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", @@ -29900,34 +28563,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/nearley": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", - "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", - "dev": true, - "dependencies": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" - }, - "bin": { - "nearley-railroad": "bin/nearley-railroad.js", - "nearley-test": "bin/nearley-test.js", - "nearley-unparse": "bin/nearley-unparse.js", - "nearleyc": "bin/nearleyc.js" - }, - "funding": { - "type": "individual", - "url": "https://nearley.js.org/#give-to-nearley" - } - }, - "node_modules/nearley/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -30776,19 +29411,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", - "dev": true, - "dependencies": { - "domhandler": "^5.0.2", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -32408,25 +31030,6 @@ "performance-now": "^2.1.0" } }, - "node_modules/railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", - "dev": true - }, - "node_modules/randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "dependencies": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -32806,19 +31409,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-shallow-renderer": { - "version": "16.15.0", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", - "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-slider": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz", @@ -33567,15 +32157,6 @@ "node": ">=8" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -33633,16 +32214,6 @@ "node": ">=10.0.0" } }, - "node_modules/rst-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==", - "dev": true, - "dependencies": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" - } - }, "node_modules/rtlcss": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz", diff --git a/package.json b/package.json index dc97829c118..bd7b7a57b34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-payments", - "version": "9.1.0", + "version": "9.2.0", "main": "webpack.config.js", "author": "Automattic", "license": "GPL-3.0-or-later", @@ -139,7 +139,7 @@ "@wordpress/html-entities": "3.6.1", "@wordpress/i18n": "4.6.1", "@wordpress/icons": "10.19.0", - "@wordpress/jest-preset-default": "8.1.2", + "@wordpress/jest-preset-default": "12.21.0", "@wordpress/plugins": "4.4.3", "@wordpress/primitives": "3.30.0", "@wordpress/scripts": "28.3.0", @@ -171,6 +171,7 @@ "iti": "0.6.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", + "jquery": "3.7.1", "lint-staged": "10.5.4", "mini-css-extract-plugin": "2.3.0", "moment": "2.29.4", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 46c8c4d4bf5..39caf7aadb4 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -36,7 +36,6 @@ - diff --git a/readme.txt b/readme.txt index dd56c5b82bf..4e7577793f3 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment Requires at least: 6.0 Tested up to: 6.7 Requires PHP: 7.3 -Stable tag: 9.1.0 +Stable tag: 9.2.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -87,6 +87,44 @@ You can read our Terms of Service and other policies [here](https://woocommerce. == Changelog == += 9.2.0 - 2025-04-09 = +* Add - Add back button for tertiary+ level pages in WooPayments settings. +* Fix - fix: cancel GooglePay/ApplePay dialog on product page if add-to-cart product validation fails +* Fix - fix: fatal error when Klarna is enabled on an EU account and a non-EU currency is configured on the store. +* Fix - fix: Google Pay/Apple Pay display on pay-for-order pages. +* Fix - Fix deprecated hook woocommerce_rest_api_option_permissions +* Fix - Fix errors in WooCommerce email settings preview +* Fix - Fix Multi-currency conversion for WooCommerce Bookings range type cost adjustments +* Fix - Fix PMME display on shortcode cart with block-based themes. +* Fix - Fix WooPay enabled during NOX onboarding despite being disabled in recommended payment methods. +* Fix - Handle pending refunds properly +* Fix - Linked account ID to product ID to maintain consistency and prevent issues when the account ID changes. +* Fix - Prevent unsaved changes dialog when changes have been saved. +* Fix - Removed hard-coded lists of payment methods where possible. +* Fix - Remove unused wcpay_date_format_notice_dismissed option from the permission list +* Fix - Set background color to white for the Payments settings page +* Fix - update: ensure Google Pay/Apple Pay honor 'Display prices during cart and checkout' setting +* Fix - Update WooPay icon on order page. +* Update - Added _wcpay_net to the metadata. +* Update - Chore: check the array type in dismissed noticeces component. +* Update - chore: disable request of JCB capability +* Update - fix: Google Pay/Apple Pay HK test address override. +* Update - fix: parsing of error message for GooglePay/ApplePay buttons to be displayed to customer, instead of displaying generic error message on failure. +* Update - Improve the ECE container loading experience. +* Update - Move payment method map definition to the backend +* Update - Prevent creation of the renewal orders if original order was created in the different WooPayments mode. +* Update - refactor: delete temporary Google Pay/Apple Pay cart contents right after making the request, to improve performance and avoiding bots sending wrong session data in subsequent requests. +* Update - Remove fraud protection discoverability and update tour +* Update - Stripe Billing and Manual Capture incompatibility notice on the Settings page. +* Update - Update Settings page as per the new design +* Update - Update to Cash App Afterpay branding. +* Dev - Bump WC tested up to version to 9.7.1. +* Dev - Fix unneeded double square brackets in the post-merge script +* Dev - Removed the deprecated wcpay_exit_survey_dismissed option from the ALLOWED_OPTIONS list. +* Dev - Remove level3 retry logic and legacy request_with_level3_data method +* Dev - Updated the progressive parameter in the KYC session creation API to use a boolean type. +* Dev - We switch to using site instead of url as the key in the self assessment data to avoid XSS firewall false-positives. + = 9.1.0 - 2025-03-19 = * Add - Add a prompt to encourage merchants to provide feedback or leave a WordPress.org review for WooPayments * Add - Add order failure message on Order Received page. diff --git a/src/Internal/Payment/PaymentRequest.php b/src/Internal/Payment/PaymentRequest.php index 79c62bdc2fc..31e69bc5e8f 100644 --- a/src/Internal/Payment/PaymentRequest.php +++ b/src/Internal/Payment/PaymentRequest.php @@ -14,6 +14,7 @@ use WCPay\Internal\Payment\PaymentMethod\PaymentMethodInterface; use WCPay\Internal\Payment\PaymentMethod\SavedPaymentMethod; use WCPay\Internal\Proxy\LegacyProxy; +use WCPay\WooPay\WooPay_Utilities; /** * Class for loading, sanitizing, and escaping data from payment requests. @@ -72,7 +73,7 @@ public function is_woopay_preflight_check(): bool { */ public function get_woopay_intent_id(): ?string { return isset( $this->request['platform-checkout-intent'] ) - ? sanitize_text_field( wp_unslash( ( $this->request['platform-checkout-intent'] ) ) ) + ? WooPay_Utilities::sanitize_intent_id( wp_unslash( ( $this->request['platform-checkout-intent'] ) ) ) : null; } diff --git a/templates/emails/customer-ipp-receipt.php b/templates/emails/customer-ipp-receipt.php index f5e78e28338..7024ed34b8e 100644 --- a/templates/emails/customer-ipp-receipt.php +++ b/templates/emails/customer-ipp-receipt.php @@ -1,5 +1,7 @@ get_billing_first_name() ) ); ?>

        get_order_number() ) ); ?>

        + -
        +
        +

        + +

        - + + + + + + + + + + + + + + + + + diff --git a/tests/e2e/env/setup.sh b/tests/e2e/env/setup.sh index eb7aaa5f607..fac99c21342 100755 --- a/tests/e2e/env/setup.sh +++ b/tests/e2e/env/setup.sh @@ -365,6 +365,9 @@ handle_permissions $WCP_ROOT/screenshots echo "Disabling rate limiter for card declined in E2E tests" cli wp option set wcpay_session_rate_limiter_disabled_wcpay_card_declined_registry yes +echo "Dismissing fraud protection welcome tour in E2E tests" +cli wp option set wcpay_fraud_protection_welcome_tour_dismissed 1 + echo "Removing all coupons ..." cli wp db query "DELETE p, m FROM wp_posts p LEFT JOIN wp_postmeta m ON p.ID = m.post_id WHERE p.post_type = 'shop_coupon'" diff --git a/tests/e2e/specs/wcpay/shopper/multi-currency-checkout.spec.ts b/tests/e2e/specs/wcpay/shopper/multi-currency-checkout.spec.ts index a0653a5791f..f326947a858 100644 --- a/tests/e2e/specs/wcpay/shopper/multi-currency-checkout.spec.ts +++ b/tests/e2e/specs/wcpay/shopper/multi-currency-checkout.spec.ts @@ -77,9 +77,9 @@ test.describe( 'Multi-currency checkout', () => { currenciesOrders[ currency ] ); await expect( - shopperPage.locator( - '.woocommerce-table--order-details tfoot tr:last-child td' - ) + shopperPage.getByRole( 'cell', { + name: /\$?\d\d[\.,]\d\d\s€?\s?[A-Z]{3}/, + } ) ).toHaveText( new RegExp( currency ) ); } ); diff --git a/tests/js/jest-test-file-setup.js b/tests/js/jest-test-file-setup.js index 09400abae27..b36ad97cb80 100644 --- a/tests/js/jest-test-file-setup.js +++ b/tests/js/jest-test-file-setup.js @@ -133,3 +133,56 @@ jest.mock( 'tracks', () => ( { isEnabled: jest.fn(), events: {}, } ) ); + +function buildMockDefinition( id, label, currencies = [], overrides = {} ) { + return { + id, + label, + description: `Mock ${ label } Description`, + settings_icon_url: `assets/images/icon-${ id }.svg`, + currencies, + stripe_key: `${ id }_payments`, + allows_manual_capture: false, + allows_pay_later: false, + accepts_only_domestic_payment: false, + ...overrides, + }; +} + +// This doesn't include all the payment methods, only the ones relevant for tests. +global.wooPaymentsPaymentMethodDefinitions = { + card: buildMockDefinition( 'card', 'Credit / Debit Cards', [], { + allows_manual_capture: true, + } ), + bancontact: buildMockDefinition( 'bancontact', 'Bancontact', [ 'EUR' ] ), + ideal: buildMockDefinition( 'ideal', 'iDEAL', [ 'EUR' ] ), + eps: buildMockDefinition( 'eps', 'EPS', [ 'EUR' ] ), + giropay: buildMockDefinition( 'giropay', 'Giropay', [ 'EUR' ] ), + sofort: buildMockDefinition( 'sofort', 'Sofort', [ 'EUR' ] ), + sepa_debit: buildMockDefinition( 'sepa_debit', 'SEPA Direct Debit', [ + 'EUR', + ] ), + p24: buildMockDefinition( 'p24', 'Przelewy24 (P24)', [ 'EUR', 'PLN' ] ), + au_becs_debit: buildMockDefinition( 'au_becs_debit', 'BECS Direct Debit', [ + 'AUD', + ] ), + affirm: buildMockDefinition( 'affirm', 'Affirm', [ 'USD', 'CAD' ], { + allows_pay_later: true, + accepts_only_domestic_payment: true, + } ), + afterpay_clearpay: buildMockDefinition( + 'afterpay_clearpay', + 'Afterpay', + [ 'USD', 'AUD', 'CAD', 'NZD', 'GBP' ], + { allows_pay_later: true, accepts_only_domestic_payment: true } + ), + klarna: buildMockDefinition( + 'klarna', + 'Klarna', + [ 'EUR', 'GBP', 'USD', 'DKK', 'NOK', 'SEK' ], + { + allows_pay_later: true, + accepts_only_domestic_payment: true, + } + ), +}; diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-option-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-option-controller.php new file mode 100644 index 00000000000..ae8333dea61 --- /dev/null +++ b/tests/unit/admin/test-class-wc-rest-payments-settings-option-controller.php @@ -0,0 +1,105 @@ +controller = new WC_REST_Payments_Settings_Option_Controller( $this->createMock( WC_Payments_API_Client::class ) ); + } + + public function test_update_option_success() { + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'option_name', 'wcpay_multi_currency_setup_completed' ); + $request->set_param( 'value', true ); + + $response = $this->controller->update_option( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( $response->get_data()['success'] ); + } + + public function provider_option_names(): array { + return [ + 'valid option: wcpay_multi_currency_setup_completed' => [ + 'wcpay_multi_currency_setup_completed', + true, + ], + 'valid option: woocommerce_dismissed_todo_tasks' => [ + 'woocommerce_dismissed_todo_tasks', + true, + ], + 'valid option: wcpay_fraud_protection_welcome_tour_dismissed' => [ + 'wcpay_fraud_protection_welcome_tour_dismissed', + true, + ], + 'invalid option: invalid_option' => [ + 'invalid_option', + false, + ], + 'invalid option: wcpay_invalid_option' => [ + 'wcpay_invalid_option', + false, + ], + ]; + } + + /** + * @dataProvider provider_option_names + */ + public function test_validate_option_name( string $option, bool $expected_result ) { + $this->assertSame( $expected_result, $this->controller->validate_option_name( $option ) ); + } + + public function test_validate_value_with_valid_values() { + $valid_values = [ + true, + false, + [], + [ 'key' => 'value' ], + [ 'key' => [ 'nested_key' => 'nested_value' ] ], + ]; + + foreach ( $valid_values as $value ) { + $result = $this->controller->validate_value( $value ); + $this->assertTrue( $result ); + } + } + + public function test_validate_value_with_invalid_values() { + $invalid_values = [ + 'string', + 123, + null, + (object) [], + ]; + + foreach ( $invalid_values as $value ) { + $result = $this->controller->validate_value( $value ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertEquals( 'rest_invalid_param', $result->get_error_code() ); + $this->assertEquals( 400, $result->get_error_data()['status'] ); + } + } +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 89ef79bfb11..859358bc89d 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -85,6 +85,7 @@ function () { require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-terminal-locations-controller.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-tos-controller.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-settings-controller.php'; + require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-settings-option-controller.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-survey-controller.php'; require_once $_plugin_dir . 'includes/admin/tracks/class-tracker.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-reader-controller.php'; diff --git a/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-two.php b/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-two.php index 0e7aba02b2c..ef9397e6069 100644 --- a/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-two.php +++ b/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-two.php @@ -34,7 +34,11 @@ public static function get_title( ?string $account_country = null ): string { return 'Second Mock Method'; } - public static function get_description(): string { + public static function get_settings_label( ?string $account_country = null ): string { + return 'Second Mock Method'; + } + + public static function get_description( ?string $account_country = null ): string { return 'Second mock payment method for testing'; } @@ -74,7 +78,7 @@ public static function get_dark_icon_url( ?string $account_country = null ): str return 'https://example.com/dark-icon.png'; } - public static function get_settings_icon_url(): string { + public static function get_settings_icon_url( ?string $account_country = null ): string { return 'https://example.com/settings-icon.png'; } diff --git a/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition.php b/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition.php index 29970aedb86..81081a75b80 100644 --- a/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition.php +++ b/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition.php @@ -34,7 +34,11 @@ public static function get_title( ?string $account_country = null ): string { return 'Mock Method'; } - public static function get_description(): string { + public static function get_settings_label( ?string $account_country = null ): string { + return 'Mock Method'; + } + + public static function get_description( ?string $account_country = null ): string { return 'Mock payment method for testing'; } @@ -74,7 +78,7 @@ public static function get_dark_icon_url( ?string $account_country = null ): str return 'https://example.com/dark-icon.png'; } - public static function get_settings_icon_url(): string { + public static function get_settings_icon_url( ?string $account_country = null ): string { return 'https://example.com/settings-icon.png'; } diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php index 4df9604fa2e..d37033667e8 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -760,8 +760,8 @@ public function test_payment_methods_show_correct_default_outputs() { $this->assertFalse( $affirm_method->is_reusable() ); $this->assertSame( 'afterpay_clearpay', $afterpay_method->get_id() ); - $this->assertSame( 'Afterpay', $afterpay_method->get_title( 'US' ) ); - $this->assertSame( 'Afterpay', $afterpay_method->get_title( 'US', $mock_afterpay_details ) ); + $this->assertSame( 'Cash App Afterpay', $afterpay_method->get_title( 'US' ) ); + $this->assertSame( 'Cash App Afterpay', $afterpay_method->get_title( 'US', $mock_afterpay_details ) ); $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $afterpay_method->is_reusable() ); $this->assertSame( 'Clearpay', $afterpay_method->get_title( 'GB' ) ); diff --git a/tests/unit/payment-methods/test-class-upe-payment-method.php b/tests/unit/payment-methods/test-class-upe-payment-method.php index 96226ac9695..bb986dd2c97 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-method.php +++ b/tests/unit/payment-methods/test-class-upe-payment-method.php @@ -83,6 +83,7 @@ public function set_up() { Link_Payment_Method::class, Affirm_Payment_Method::class, Afterpay_Payment_Method::class, + Klarna_Payment_Method::class, ]; foreach ( $payment_method_classes as $payment_method_class ) { @@ -123,6 +124,52 @@ public function test_get_countries( string $payment_method_id, array $expected_r $this->assertEquals( $expected_result, $payment_method->get_countries() ); } + public function test_klarna_get_countries_with_eu_country_and_eu_currency() { + WC_Helper_Site_Currency::$mock_site_currency = 'EUR'; + $payment_method = $this->mock_payment_methods['klarna']; + + $this->mock_wcpay_account->method( 'get_cached_account_data' )->willReturn( + [ + 'country' => 'DE', + ] + ); + + $this->assertEquals( + [ + 'AT', + 'BE', + 'FI', + 'FR', + 'DE', + 'IE', + 'IT', + 'NL', + 'ES', + ], + $payment_method->get_countries() + ); + WC_Helper_Site_Currency::$mock_site_currency = ''; + } + + public function test_klarna_get_countries_with_eu_country_and_non_eu_currency() { + WC_Helper_Site_Currency::$mock_site_currency = 'AUD'; + $payment_method = $this->mock_payment_methods['klarna']; + + $this->mock_wcpay_account->method( 'get_cached_account_data' )->willReturn( + [ + 'country' => 'IT', + ] + ); + + $this->assertEquals( + [ + 'NONE_SUPPORTED', + ], + $payment_method->get_countries() + ); + WC_Helper_Site_Currency::$mock_site_currency = ''; + } + public function provider_test_get_countries() { return [ 'Payment method without countries' => [ diff --git a/tests/unit/src/Internal/Payment/PaymentRequestTest.php b/tests/unit/src/Internal/Payment/PaymentRequestTest.php index 2d1ac129b74..d58d2dcc9cd 100644 --- a/tests/unit/src/Internal/Payment/PaymentRequestTest.php +++ b/tests/unit/src/Internal/Payment/PaymentRequestTest.php @@ -49,15 +49,6 @@ public function test_get_fraud_prevention_token( ?string $value, ?string $expect $this->assertSame( $expected, $this->sut->get_fraud_prevention_token() ); } - /** - * @dataProvider provider_text_string_param - */ - public function test_get_woopay_intent_id( ?string $value, ?string $expected ) { - $request = is_null( $value ) ? [] : [ 'platform-checkout-intent' => $value ]; - $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); - $this->assertSame( $expected, $this->sut->get_woopay_intent_id() ); - } - /** * @dataProvider provider_text_string_param */ @@ -97,6 +88,36 @@ public function provider_text_string_param(): array { ]; } + /** + * @dataProvider provider_get_woopay_intent_id + */ + public function test_get_woopay_intent_id( ?string $value, ?string $expected ) { + $request = is_null( $value ) ? [] : [ 'platform-checkout-intent' => $value ]; + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); + $this->assertSame( $expected, $this->sut->get_woopay_intent_id() ); + } + + public function provider_get_woopay_intent_id(): array { + return [ + 'Param is not set' => [ + 'value' => null, + 'expected' => null, + ], + 'empty string' => [ + 'value' => '', + 'expected' => '', + ], + 'normal string' => [ + 'value' => 'String-with-dash_and_underscore', + 'expected' => 'Stringwithdash_and_underscore', + ], + 'string will be changed after sanitization' => [ + 'value' => " \nString-with_special_chars__@.#$%^&*()", + 'expected' => 'tagStringwith_special_chars__', + ], + ]; + } + public function provider_text_string_for_bool_representation(): array { return [ 'Param is not set' => [ diff --git a/tests/unit/subscriptions/test-class-wc-payments-invoice-service.php b/tests/unit/subscriptions/test-class-wc-payments-invoice-service.php index 1518f571285..b7e33c2a4e2 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-invoice-service.php +++ b/tests/unit/subscriptions/test-class-wc-payments-invoice-service.php @@ -189,29 +189,29 @@ public function test_validate_invoice_with_valid_data() { 'subscription_item' => 'si_test123_line_item', 'quantity' => 4, 'price' => - [ - 'unit_amount_decimal' => 1000, - 'currency' => 'usd', - 'recurring' => [ - 'interval' => 'month', - 'interval_count' => 1, + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + 'recurring' => + [ + 'interval' => 'month', + 'interval_count' => 1, + ], ], - ], ], [ 'subscription_item' => 'si_test123_shipping', 'quantity' => 1, 'price' => - [ - 'unit_amount_decimal' => 1000, - 'currency' => 'usd', - 'recurring' => [ - 'interval' => 'month', - 'interval_count' => 1, + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + 'recurring' => + [ + 'interval' => 'month', + 'interval_count' => 1, + ], ], - ], ], ]; @@ -225,7 +225,53 @@ public function test_validate_invoice_with_valid_data() { ->expects( $this->never() ) ->method( 'update_subscription' ); - $this->invoice_service->validate_invoice( $mock_item_data, $mock_discount_data, $mock_subscription ); + $invoice_service = $this->getMockBuilder( WC_Payments_Invoice_Service::class ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_product_service, $this->mock_order_service ] ) + ->onlyMethods( [ 'get_recurring_items', 'get_wcpay_item_id' ] ) + ->getMock(); + + $subscription_items = []; + foreach ( $mock_order->get_items( [ 'line_item', 'fee', 'shipping' ] ) as $item ) { + $subscription_items[ $item->get_id() ] = $item; + } + + $invoice_service + ->expects( $this->once() ) + ->method( 'get_recurring_items' ) + ->willReturn( + [ + [ + 'quantity' => 4, + 'price_data' => [ + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + ], + 'metadata' => [ + 'wc_item_id' => key( $subscription_items ), + ], + ], + [ + 'quantity' => 1, + 'price_data' => [ + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + ], + 'metadata' => [ + 'wc_item_id' => array_keys( $subscription_items )[1], + ], + ], + ] + ); + + $invoice_service + ->method( 'get_wcpay_item_id' ) + ->willReturnCallback( + function ( $item ) { + return 'si_test123_' . $item->get_type(); + } + ); + + $invoice_service->validate_invoice( $mock_item_data, $mock_discount_data, $mock_subscription ); } /** @@ -247,29 +293,29 @@ public function test_validate_invoice_with_invalid_data() { 'subscription_item' => 'si_test123_line_item', 'quantity' => 1, 'price' => - [ - 'unit_amount_decimal' => 1000, - 'currency' => 'usd', - 'recurring' => [ - 'interval' => 'month', - 'interval_count' => 1, + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + 'recurring' => + [ + 'interval' => 'month', + 'interval_count' => 1, + ], ], - ], ], [ 'subscription_item' => 'si_test123_shipping', 'quantity' => 1, 'price' => - [ - 'unit_amount_decimal' => 1000, - 'currency' => 'usd', - 'recurring' => [ - 'interval' => 'month', - 'interval_count' => 1, + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + 'recurring' => + [ + 'interval' => 'month', + 'interval_count' => 1, + ], ], - ], ], ]; @@ -298,7 +344,58 @@ public function test_validate_invoice_with_invalid_data() { [ 'discounts' => [] ] ); - $this->invoice_service->validate_invoice( $mock_item_data, $mock_discount_data, $mock_subscription ); + // Create a partial mock of the invoice service. + $invoice_service = $this->getMockBuilder( WC_Payments_Invoice_Service::class ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_product_service, $this->mock_order_service ] ) + ->onlyMethods( [ 'get_recurring_items', 'get_wcpay_item_id' ] ) + ->getMock(); + + // Create a map of subscription items. + $subscription_items = []; + foreach ( $mock_order->get_items( [ 'line_item', 'fee', 'shipping' ] ) as $item ) { + $subscription_items[ $item->get_id() ] = $item; + } + + // Mock get_recurring_items to return data that will trigger a quantity mismatch (4 instead of 1). + $invoice_service + ->expects( $this->once() ) + ->method( 'get_recurring_items' ) + ->willReturn( + [ + [ + 'quantity' => 4, // Different quantity than in mock_item_data (1). + 'price_data' => [ + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + ], + 'metadata' => [ + 'wc_item_id' => key( $subscription_items ), // First item ID (line_item). + ], + ], + [ + 'quantity' => 1, + 'price_data' => [ + 'unit_amount_decimal' => 1000, + 'currency' => 'usd', + ], + 'metadata' => [ + 'wc_item_id' => array_keys( $subscription_items )[1], // Second item ID. + ], + ], + ] + ); + + // Mock get_wcpay_item_id to return the correct ID based on item type. + $invoice_service + ->expects( $this->exactly( 2 ) ) + ->method( 'get_wcpay_item_id' ) + ->willReturnCallback( + function ( $item ) { + return 'si_test123_' . $item->get_type(); + } + ); + + $invoice_service->validate_invoice( $mock_item_data, $mock_discount_data, $mock_subscription ); $this->assertSame( [], $mock_subscription->get_meta( self::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, true ) ); } diff --git a/tests/unit/subscriptions/test-class-wc-payments-product-service.php b/tests/unit/subscriptions/test-class-wc-payments-product-service.php index 67f4c8e1c20..b07dcaa3cef 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-product-service.php +++ b/tests/unit/subscriptions/test-class-wc-payments-product-service.php @@ -6,7 +6,6 @@ */ use PHPUnit\Framework\MockObject\MockObject; -use WCPay\Exceptions\API_Exception; /** * WC_Payments_Product_Service unit tests. @@ -31,15 +30,28 @@ class WC_Payments_Product_Service_Test extends WCPAY_UnitTestCase { */ private $mock_api_client; + /** + * @var WC_Payments_Account|MockObject + */ + private $mock_account_service; + + /** + * Mock product. + * + * @var WC_Product|MockObject + */ + private $mock_product; + /** * Pre-test setup */ public function set_up() { parent::set_up(); - $this->mock_product = $this->get_mock_product(); - $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); - $this->product_service = new WC_Payments_Product_Service( $this->mock_api_client ); + $this->mock_product = $this->get_mock_product(); + $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); + $this->mock_account_service = $this->createMock( WC_Payments_Account::class ); + $this->product_service = new WC_Payments_Product_Service( $this->mock_api_client, $this->mock_account_service ); WC_Payments::mode()->live(); } @@ -52,33 +64,51 @@ public function tear_down() { * Test create product. */ public function test_create_product() { + $account_id = 'acct_test123'; + $product_id = 'prod_test123'; $this->mock_api_client->expects( $this->once() ) ->method( 'create_product' ) ->with( $this->get_mock_product_data() ) - ->willReturn( [ 'wcpay_product_id' => 'prod_test123' ] ); + ->willReturn( [ 'wcpay_product_id' => $product_id ] ); + + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); $this->mock_get_period( 'month' ); $this->mock_get_interval( 3 ); $this->product_service->create_product( $this->mock_product ); - $this->assertEquals( 'prod_test123', $this->mock_product->get_meta( self::LIVE_PRODUCT_ID_KEY, true ) ); + + $this->assertSame( $product_id, $this->mock_product->get_meta( self::LIVE_PRODUCT_ID_KEY ) ); + $this->assertSame( $account_id, $this->mock_product->get_meta( self::LIVE_PRODUCT_ID_KEY . '_linked_to' ) ); } /** * Test update product. */ public function test_update_products_live_only() { - $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY, 'prod_test123' ); + $mock_product_id = 'prod_1234'; + $account_id = 'acct_test123'; + + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY, $mock_product_id ); + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY . '_linked_to', $account_id ); $this->mock_product->save(); + $this->mock_account_service + ->expects( $this->exactly( 2 ) ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); + $this->mock_api_client->expects( $this->once() ) ->method( 'update_product' ) ->with( - 'prod_test123', + $mock_product_id, $this->get_mock_product_data( [ 'test_mode' => false ] ) ) ->willReturn( [ - 'wcpay_product_id' => 'prod_test123', + 'wcpay_product_id' => $mock_product_id, 'wcpay_price_id' => 'price_test123', ] ); @@ -92,19 +122,30 @@ public function test_update_products_live_only() { * Test update product. */ public function test_update_products_live_and_test() { - $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY, 'prod_test123_live' ); - $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY, 'prod_test123_test' ); + $mock_prod_product_id = 'prod_1234'; + $mock_test_product_id = 'prod_5678'; + $account_id = 'acct_test123'; + + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY, $mock_prod_product_id ); + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY . '_linked_to', $account_id ); + $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY, $mock_test_product_id ); + $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY . '_linked_to', $account_id ); $this->mock_product->save(); + $this->mock_account_service + ->expects( $this->exactly( 4 ) ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); + $this->mock_api_client->expects( $this->exactly( 2 ) ) ->method( 'update_product' ) ->withConsecutive( [ - 'prod_test123_live', + $mock_prod_product_id, $this->get_mock_product_data( [ 'test_mode' => false ] ), ], [ - 'prod_test123_test', + $mock_test_product_id, $this->get_mock_product_data( [ 'test_mode' => true ] ), ] ) @@ -126,11 +167,18 @@ public function test_update_products_live_and_test() { * Note: This also tests unarchive_product */ public function test_archive_product() { + $account_id = 'acct_1234'; $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY, 'prod_test123' ); + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY . '_linked_to', $account_id ); $this->mock_product->update_meta_data( self::LIVE_PRICE_ID_KEY, 'price_test123' ); $this->mock_product->update_meta_data( self::TEST_PRICE_ID_KEY, 'price_test456' ); $this->mock_product->save(); + $this->mock_account_service + ->expects( $this->exactly( 2 ) ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); + $this->mock_api_client->expects( $this->once() ) ->method( 'update_product' ) ->with( @@ -170,7 +218,6 @@ public function test_archive_product() { // Confirm that the product price IDs have been deleted. $this->assertFalse( $this->mock_product->meta_exists( self::LIVE_PRICE_ID_KEY ) ); - $this->assertFalse( $this->mock_product->meta_exists( self::LIVE_PRICE_ID_KEY ) ); } /** @@ -242,40 +289,92 @@ private function mock_get_interval( $interval ) { WC_Subscriptions_Product::set_interval( $interval ); } - /** - * Tests for WC_Payments_Product_Service::get_wcpay_product_id() - */ - public function test_get_wcpay_product_id() { - WC_Subscriptions_Product::$is_subscription = true; + public function test_get_or_create_wcpay_product_id_for_test() { + $mock_product_id = 'prod_123'; + $account_id = 'acct_test123'; - // Make sure the WC_Payments_Subscriptions::get_product_service() returns our mock product service object. - $ref = new ReflectionProperty( 'WC_Payments_Subscriptions', 'product_service' ); - $ref->setAccessible( true ); - $ref->setValue( null, $this->product_service ); + $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY, $mock_product_id ); + $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY . '_linked_to', $account_id ); + $this->mock_product->save(); - $mock_product_id = 'prod_123_wcpay_test_product_id'; - $this->mock_product->update_meta_data( WC_Payments_Product_Service::LIVE_PRODUCT_ID_KEY, $mock_product_id ); + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); - $this->assertSame( $mock_product_id, $this->product_service->get_wcpay_product_id( $this->mock_product ) ); + $this->mock_api_client + ->expects( $this->never() ) + ->method( 'create_product' ); - // Test that deleting the price will cause the product to be created. - $this->mock_product->delete_meta_data( WC_Payments_Product_Service::LIVE_PRODUCT_ID_KEY ); - $this->mock_api_client->expects( $this->once() ) + $result = $this->product_service->get_or_create_wcpay_product_id( $this->mock_product, true ); + + $this->assertEquals( $mock_product_id, $result ); + } + + public function test_get_or_create_wcpay_product_id_for_live() { + $mock_product_id = 'prod_123'; + $account_id = 'acct_test123'; + + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY, $mock_product_id ); + $this->mock_product->update_meta_data( self::LIVE_PRODUCT_ID_KEY . '_linked_to', $account_id ); + $this->mock_product->save(); + + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); + + $this->mock_api_client + ->expects( $this->never() ) + ->method( 'create_product' ); + + $result = $this->product_service->get_or_create_wcpay_product_id( $this->mock_product, false ); + + $this->assertEquals( $mock_product_id, $result ); + } + public function test_get_or_create_wcpay_product_id_will_create_product_if_not_exist() { + $new_product_id = 'prod_test123'; + $account_id = 'acct_test123'; + + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); + + $this->mock_api_client + ->expects( $this->once() ) ->method( 'create_product' ) ->with( $this->get_mock_product_data() ) - ->willReturn( - [ - 'wcpay_product_id' => $mock_product_id, - 'wcpay_price_id' => 'price_test123', - ] - ); + ->willReturn( [ 'wcpay_product_id' => $new_product_id ] ); - $this->mock_get_period( 'month' ); - $this->mock_get_interval( 3 ); + $result = $this->product_service->get_or_create_wcpay_product_id( $this->mock_product ); - $this->assertSame( $mock_product_id, $this->product_service->get_wcpay_product_id( $this->mock_product ) ); + $this->assertEquals( $new_product_id, $result ); } + public function test_create_new_product_for_different_account() { + $mock_product_id = 'prod_123'; + $new_product_id = 'prod_456'; + + $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY, $mock_product_id ); + $this->mock_product->update_meta_data( self::TEST_PRODUCT_ID_KEY . '_linked_to', 'acct_test123' ); + $this->mock_product->save(); + + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'get_stripe_account_id' ) + ->willReturn( 'acct_test456' ); + + $this->mock_api_client + ->expects( $this->once() ) + ->method( 'create_product' ) + ->with( $this->get_mock_product_data() ) + ->willReturn( [ 'wcpay_product_id' => $new_product_id ] ); + + $result = $this->product_service->get_or_create_wcpay_product_id( $this->mock_product ); + + $this->assertEquals( $new_product_id, $result ); + } /** * Tests for WC_Payments_Product_Service::get_wcpay_product_id_option() */ @@ -291,20 +390,27 @@ public function test_get_wcpay_product_id_option() { * Tests for WC_Payments_Product_Service::get_wcpay_product_id_for_item() */ public function test_get_wcpay_product_id_for_item() { + $product_id = 'product_id_test123'; + $account_id = 'acct_test123'; $this->mock_api_client->expects( $this->once() ) ->method( 'create_product' ) ->willReturn( [ - 'wcpay_product_id' => 'product_id_test123', + 'wcpay_product_id' => $product_id, 'wcpay_price_id' => 'price_test123', ] ); + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'get_stripe_account_id' ) + ->willReturn( $account_id ); + // If type is 'Test Tax *&^ name', the result should be _wcpay_product_id_live_test_tax__name. $test_type = 'Test Tax *&^ name'; $this->product_service->get_wcpay_product_id_for_item( $test_type ); $this->assertFalse( get_option( '_wcpay_product_id_live_Test Tax *&^ name' ) ); - $this->assertSame( 'product_id_test123', get_option( '_wcpay_product_id_live_test_tax__name' ) ); + $this->assertSame( $product_id, get_option( '_wcpay_product_id_live_test_tax__name' ) ); } } diff --git a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php index 490d1c74298..d9abecb8f9a 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php +++ b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php @@ -6,6 +6,9 @@ */ use PHPUnit\Framework\MockObject\MockObject; +use WCPay\Constants\Order_Mode; +use WCPay\Core\Mode; +use WCPay\Exceptions\Subscription_Mode_Mismatch_Exception; /** * WC_Payments_Subscription_Service_Test unit tests. @@ -188,7 +191,7 @@ public function test_create_subscription() { ->willReturn( $mock_wcpay_product_id ); $this->mock_product_service->expects( $this->once() ) - ->method( 'get_wcpay_product_id' ) + ->method( 'get_or_create_wcpay_product_id' ) ->willReturn( $mock_wcpay_product_id ); $this->mock_product_service->method( 'is_valid_billing_cycle' )->willReturn( true ); @@ -268,7 +271,7 @@ function ( $id ) use ( $mock_subscription ) { ->willReturn( 'wcpay_prod_test123' ); $this->mock_product_service->expects( $this->once() ) - ->method( 'get_wcpay_product_id' ) + ->method( 'get_or_create_wcpay_product_id' ) ->willReturn( 'wcpay_prod_test123' ); $this->mock_api_client->expects( $this->once() ) @@ -773,4 +776,35 @@ public function test_format_item_price_data() { $this->assertEquals( $expected, $actual ); } + + /** + * Test WC_Payments_Subscription_Service->check_wcpay_mode_for_subscription() + */ + public function test_check_wcpay_mode_for_subscription() { + $mock_order = WC_Helper_Order::create_order(); + $mock_subscription = new WC_Subscription(); + $mock_subscription->set_parent( $mock_order ); + $mock_order->update_meta_data( WC_Payments_Order_Service::WCPAY_MODE_META_KEY, Order_Mode::TEST ); + + WC_Payments::mode()->test(); + + $items = [ 'item1', 'item2' ]; + $result = $this->subscription_service->check_wcpay_mode_for_subscription( $items, $mock_order, $mock_subscription ); + $this->assertEquals( $items, $result ); + + WC_Payments::mode()->live(); + + $this->expectException( Subscription_Mode_Mismatch_Exception::class ); + $this->expectExceptionMessage( 'Subscription was made when WooPayments was in the test mode and cannot be renewed in the live mode.' ); + $this->subscription_service->check_wcpay_mode_for_subscription( $items, $mock_order, $mock_subscription ); + + $mock_order->update_meta_data( WC_Payments_Order_Service::WCPAY_MODE_META_KEY, Order_Mode::PRODUCTION ); + $result = $this->subscription_service->check_wcpay_mode_for_subscription( $items, $mock_order, $mock_subscription ); + $this->assertEquals( $items, $result ); + + WC_Payments::mode()->test(); + $this->expectException( Subscription_Mode_Mismatch_Exception::class ); + $this->expectExceptionMessage( 'Subscription was made when WooPayments was in the live mode and cannot be renewed in the test mode.' ); + $this->subscription_service->check_wcpay_mode_for_subscription( $items, $mock_order, $mock_subscription ); + } } diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php b/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php index 46576e34d41..29e55d4ce8b 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php @@ -103,7 +103,7 @@ public function set_up() { // Note that we cannot use createStub here since it's not defined in PHPUnit 6.5. $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) ->disableOriginalConstructor() - ->setMethods( [ 'create_and_confirm_intention', 'get_payment_method', 'request_with_level3_data', 'is_server_connected' ] ) + ->setMethods( [ 'create_and_confirm_intention', 'get_payment_method', 'is_server_connected' ] ) ->getMock(); // Arrange: Mock WC_Payments_Account instance to use later. diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php index f3a0aefa442..6db39741281 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php @@ -188,6 +188,7 @@ public function test_process_refund_should_work_without_payment_method_id_meta() 'id' => 're_123456789', 'amount' => $amount, 'currency' => 'usd', + 'status' => 'succeeded', ] ); $request = $this->mock_wcpay_request( Refund_Charge::class ); diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index ef0bc69c0cc..003a808fa14 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -689,7 +689,7 @@ public function test_payment_methods_show_correct_default_outputs() { $this->assertSame( 'afterpay_clearpay', $afterpay_method->get_id() ); $this->assertSame( 'Afterpay', $afterpay_method->get_title() ); - $this->assertSame( 'Afterpay', $afterpay_method->get_title( 'US', $mock_afterpay_details ) ); + $this->assertSame( 'Cash App Afterpay', $afterpay_method->get_title( 'US', $mock_afterpay_details ) ); $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $afterpay_method->is_reusable() ); @@ -2441,6 +2441,7 @@ public function test_outputs_payments_settings_screen() { $this->card_gateway->output_payments_settings_screen(); $output = ob_get_clean(); $this->assertStringMatchesFormat( '%aid="wcpay-account-settings-container"%a', $output ); + $this->assertStringMatchesFormat( '%ahref="admin.php?page=wc-settings&tab=checkout"%a', $output ); } public function test_outputs_express_checkout_settings_screen() { @@ -2450,6 +2451,7 @@ public function test_outputs_express_checkout_settings_screen() { $output = ob_get_clean(); $this->assertStringMatchesFormat( '%aid="wcpay-express-checkout-settings-container"%a', $output ); $this->assertStringMatchesFormat( '%adata-method-id="foo"%a', $output ); + $this->assertStringMatchesFormat( '%ahref="admin.php?page=wc-settings&tab=checkout&section=woocommerce_payments"%a', $output ); } /** @@ -3942,6 +3944,10 @@ private function prepare_gateway_for_availability_testing( $gateway ) { private function init_payment_methods() { $payment_methods = []; + /** + * FLAG: PAYMENT_METHODS_LIST + * As payment methods are converted to use definitions, they need to be removed from the list below. + */ $payment_method_classes = [ CC_Payment_Method::class, Bancontact_Payment_Method::class, diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index 2ce778d4bdc..c731a3de7ea 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -937,6 +937,11 @@ public function test_ensure_woopay_enabled_by_default_value_set_in_sandbox_mode_ ] ); + $this->mock_onboarding_service + ->expects( $this->once() ) + ->method( 'should_enable_woopay' ) + ->willReturn( true ); + $original_value = get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ); // Act. diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php index b2ee7a0260c..0e8e540567d 100644 --- a/tests/unit/test-class-wc-payments-checkout.php +++ b/tests/unit/test-class-wc-payments-checkout.php @@ -388,21 +388,25 @@ public function test_link_payment_method_provided_when_card_enabled() { [ 'card' => [ 'isReusable' => true, + 'isBnpl' => false, 'title' => 'Cards', 'icon' => $icon_url, 'darkIcon' => $dark_icon_url, 'showSaveOption' => true, 'countries' => [], + 'gatewayId' => 'woocommerce_payments', 'testingInstructions' => 'Use test card or refer to our testing guide.', 'forceNetworkSavedCards' => false, ], 'link' => [ 'isReusable' => true, + 'isBnpl' => false, 'title' => 'Link', 'icon' => $icon_url, 'darkIcon' => $dark_icon_url, 'showSaveOption' => true, 'countries' => [], + 'gatewayId' => 'woocommerce_payments_link', 'testingInstructions' => '', 'forceNetworkSavedCards' => false, ], diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php index 1ab8593f879..3e507472fdf 100644 --- a/tests/unit/test-class-wc-payments-order-service.php +++ b/tests/unit/test-class-wc-payments-order-service.php @@ -10,6 +10,7 @@ use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Method; use WCPay\Fraud_Prevention\Models\Rule; +use WCPay\Constants\Refund_Status; /** * WC_Payments_Order_Service unit tests. @@ -1394,7 +1395,7 @@ public function test_attach_transaction_fee_to_order_null_fee() { $this->order_service->attach_transaction_fee_to_order( $mock_order, new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, null, [], [], 'eur' ) ); } - public function test_add_note_and_metadata_for_refund_fully_refunded(): void { + public function test_add_note_and_metadata_for_created_refund_successful_fully_refunded(): void { $order = WC_Helper_Order::create_order(); $order->save(); @@ -1405,12 +1406,13 @@ public function test_add_note_and_metadata_for_refund_fully_refunded(): void { $wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, $order->get_items() ); - $this->order_service->add_note_and_metadata_for_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); $order_note = wc_get_order_notes( [ 'order_id' => $order->get_id() ] )[0]->content; $this->assertStringContainsString( $refunded_amount, $order_note, 'Order note does not contain expected refund amount' ); $this->assertStringContainsString( $refund_id, $order_note, 'Order note does not contain expected refund id' ); $this->assertStringContainsString( $refund_reason, $order_note, 'Order note does not contain expected refund reason' ); + $this->assertStringContainsString( 'was successfully processed', $order_note, 'Order note should indicate successful processing' ); $this->assertSame( 'successful', $order->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_STATUS_META_KEY, true ) ); $this->assertSame( $refund_id, $wc_refund->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_ID_META_KEY, true ) ); @@ -1419,7 +1421,7 @@ public function test_add_note_and_metadata_for_refund_fully_refunded(): void { WC_Helper_Order::delete_order( $order->get_id() ); } - public function test_add_note_and_metadata_for_refund_partially_refunded(): void { + public function test_add_note_and_metadata_for_created_refund_successful_partially_refunded(): void { $order = WC_Helper_Order::create_order(); $order->save(); @@ -1429,7 +1431,7 @@ public function test_add_note_and_metadata_for_refund_partially_refunded(): void $refund_balance_transaction_id = 'txn_1J2a3B4c5D6e7F8g9H0'; $wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, $order->get_items() ); - $this->order_service->add_note_and_metadata_for_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); $this->assertSame( Order_Status::PENDING, $order->get_status() ); @@ -1437,6 +1439,7 @@ public function test_add_note_and_metadata_for_refund_partially_refunded(): void $this->assertStringContainsString( $refunded_amount, $order_note, 'Order note does not contain expected refund amount' ); $this->assertStringContainsString( $refund_id, $order_note, 'Order note does not contain expected refund id' ); $this->assertStringContainsString( $refund_reason, $order_note, 'Order note does not contain expected refund reason' ); + $this->assertStringContainsString( 'was successfully processed', $order_note, 'Order note should indicate successful processing' ); $this->assertSame( 'successful', $order->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_STATUS_META_KEY, true ) ); $this->assertSame( $refund_id, $wc_refund->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_ID_META_KEY, true ) ); @@ -1445,6 +1448,57 @@ public function test_add_note_and_metadata_for_refund_partially_refunded(): void WC_Helper_Order::delete_order( $order->get_id() ); } + public function test_add_note_and_metadata_for_created_refund_pending(): void { + $order = WC_Helper_Order::create_order(); + $order->save(); + + $refunded_amount = 50; + $refund_id = 're_1J2a3B4c5D6e7F8g9H0'; + $refund_reason = 'Test refund'; + $refund_balance_transaction_id = 'txn_1J2a3B4c5D6e7F8g9H0'; + + $wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, $order->get_items() ); + + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id, true ); + + $order_note = wc_get_order_notes( [ 'order_id' => $order->get_id() ] )[0]->content; + $this->assertStringContainsString( $refunded_amount, $order_note, 'Order note does not contain expected refund amount' ); + $this->assertStringContainsString( $refund_id, $order_note, 'Order note does not contain expected refund id' ); + $this->assertStringContainsString( $refund_reason, $order_note, 'Order note does not contain expected refund reason' ); + $this->assertStringContainsString( 'is pending', $order_note, 'Order note should indicate pending status' ); + $this->assertStringContainsString( 'https://woocommerce.com/document/woopayments/managing-money/#pending-refunds', $order_note, 'Order note should contain link to pending refunds documentation' ); + + $this->assertSame( Refund_Status::PENDING, $order->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_STATUS_META_KEY, true ) ); + $this->assertSame( $refund_id, $wc_refund->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_ID_META_KEY, true ) ); + $this->assertSame( $refund_balance_transaction_id, $order->get_refunds()[0]->get_meta( WC_Payments_Order_Service::WCPAY_REFUND_TRANSACTION_ID_META_KEY, true ) ); + + WC_Helper_Order::delete_order( $order->get_id() ); + } + + public function test_add_note_and_metadata_for_created_refund_no_duplicate_notes(): void { + $order = WC_Helper_Order::create_order(); + $order->save(); + + $refunded_amount = 50; + $refund_id = 're_1J2a3B4c5D6e7F8g9H0'; + $refund_reason = 'Test refund'; + $refund_balance_transaction_id = 'txn_1J2a3B4c5D6e7F8g9H0'; + + $wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, $order->get_items() ); + + // Add note first time. + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); + $initial_notes_count = count( wc_get_order_notes( [ 'order_id' => $order->get_id() ] ) ); + + // Add note second time. + $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id ); + $final_notes_count = count( wc_get_order_notes( [ 'order_id' => $order->get_id() ] ) ); + + $this->assertSame( $initial_notes_count, $final_notes_count, 'Duplicate notes should not be added' ); + + WC_Helper_Order::delete_order( $order->get_id() ); + } + public function test_process_captured_payment() { $order = WC_Helper_Order::create_order(); $order->save(); @@ -1473,4 +1527,165 @@ public function test_process_captured_payment() { $notes_2 = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); $this->assertEquals( count( $notes ), count( $notes_2 ) ); } + + /** + * Tests handling of failed refunds. + * + * @dataProvider provider_handle_failed_refund + */ + public function test_handle_failed_refund( string $initial_order_status, bool $has_refund, bool $expect_status_change ): void { + // Arrange: Create order and optionally add a refund. + $order = WC_Helper_Order::create_order(); + $wc_refund = null; + if ( $has_refund ) { + $wc_refund = $this->order_service->create_refund_for_order( $order, $order->get_total(), 'Test refund reason', $order->get_items() ); + } + $order->set_status( $initial_order_status ); + $order->save(); + + $refund_id = 're_123456789'; + $amount = 1000; // $10.00 + $currency = 'usd'; + + // Act: Handle the failed refund. + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $wc_refund ); + + // Assert: Check order status was updated if needed. + if ( $expect_status_change ) { + $this->assertTrue( $order->has_status( Order_Status::FAILED ) ); + } else { + $this->assertTrue( $order->has_status( $initial_order_status ) ); + } + + // Assert: Check refund status was set to failed. + $this->assertSame( Refund_Status::FAILED, $this->order_service->get_wcpay_refund_status_for_order( $order ) ); + + // Assert: Check order note was added. + $notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); + $this->assertStringContainsString( 'unsuccessful', $notes[0]->content ); + $this->assertStringContainsString( $refund_id, $notes[0]->content ); + + // Assert: If refund existed, it was deleted. + if ( $has_refund ) { + $this->assertEmpty( $order->get_refunds() ); + } + + WC_Helper_Order::delete_order( $order->get_id() ); + } + + public function provider_handle_failed_refund(): array { + return [ + 'Order not refunded - no status change' => [ + 'initial_order_status' => Order_Status::PROCESSING, + 'has_refund' => false, + 'expect_status_change' => false, + ], + 'Order fully refunded - status changes to failed' => [ + 'initial_order_status' => Order_Status::REFUNDED, + 'has_refund' => true, + 'expect_status_change' => true, + ], + 'Order partially refunded - no status change' => [ + 'initial_order_status' => Order_Status::PROCESSING, + 'has_refund' => true, + 'expect_status_change' => false, + ], + ]; + } + + /** + * Tests that handle_failed_refund doesn't add duplicate notes. + */ + public function test_handle_failed_refund_no_duplicate_notes(): void { + // Arrange: Create order and handle failed refund twice. + $order = WC_Helper_Order::create_order(); + $order->save(); + + $refund_id = 're_123456789'; + $amount = 1000; + $currency = 'usd'; + + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency ); + $initial_notes_count = count( wc_get_order_notes( [ 'order_id' => $order->get_id() ] ) ); + + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency ); + $final_notes_count = count( wc_get_order_notes( [ 'order_id' => $order->get_id() ] ) ); + + // Assert: No duplicate notes were added. + $this->assertSame( $initial_notes_count, $final_notes_count ); + + WC_Helper_Order::delete_order( $order->get_id() ); + } + + /** + * Tests that handle_failed_refund adds the correct note for cancelled refunds. + */ + public function test_handle_failed_refund_cancelled(): void { + // Arrange: Create order and handle cancelled refund. + $order = WC_Helper_Order::create_order(); + $order->save(); + + $refund_id = 're_123456789'; + $amount = 1000; + $currency = 'usd'; + + // Act: Handle the cancelled refund. + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, null, true ); + + // Assert: Check order note was added with cancelled status. + $notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); + $this->assertStringContainsString( 'cancelled', $notes[0]->content ); + $this->assertStringContainsString( $refund_id, $notes[0]->content ); + + // Assert: Check refund status was set to failed. + $this->assertSame( Refund_Status::FAILED, $this->order_service->get_wcpay_refund_status_for_order( $order ) ); + + WC_Helper_Order::delete_order( $order->get_id() ); + } + + /** + * Tests that handle_failed_refund doesn't add duplicate notes for cancelled refunds. + */ + public function test_handle_failed_refund_cancelled_no_duplicate_notes(): void { + // Arrange: Create order and handle cancelled refund twice. + $order = WC_Helper_Order::create_order(); + $order->save(); + + $refund_id = 're_123456789'; + $amount = 1000; + $currency = 'usd'; + + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, null, true ); + $initial_notes_count = count( wc_get_order_notes( [ 'order_id' => $order->get_id() ] ) ); + + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, null, true ); + $final_notes_count = count( wc_get_order_notes( [ 'order_id' => $order->get_id() ] ) ); + + // Assert: No duplicate notes were added. + $this->assertSame( $initial_notes_count, $final_notes_count ); + + WC_Helper_Order::delete_order( $order->get_id() ); + } + + /** + * Tests that handle_failed_refund updates order status to failed when fully refunded. + */ + public function test_handle_failed_refund_cancelled_updates_order_status(): void { + // Arrange: Create order and set it to refunded status. + $order = WC_Helper_Order::create_order(); + $order->set_status( Order_Status::REFUNDED ); + $order->save(); + + $refund_id = 're_123456789'; + $amount = 1000; + $currency = 'usd'; + + // Act: Handle the cancelled refund. + $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, null, true ); + + // Assert: Order status was updated to failed. + $this->assertTrue( $order->has_status( Order_Status::FAILED ) ); + + WC_Helper_Order::delete_order( $order->get_id() ); + } } diff --git a/tests/unit/test-class-wc-payments-payment-request-session.php b/tests/unit/test-class-wc-payments-payment-request-session.php index 0cb49755dbc..77548103752 100644 --- a/tests/unit/test-class-wc-payments-payment-request-session.php +++ b/tests/unit/test-class-wc-payments-payment-request-session.php @@ -42,31 +42,49 @@ public function tear_down() { } public function test_adds_tokenized_session_headers() { - $_SERVER['REQUEST_URI'] = '/index.php'; - $_REQUEST['rest_route'] = '/wc/store/v1/cart'; - $request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' ); + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION_NONCE'] = wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ); + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] = ''; + $_SERVER['REQUEST_URI'] = '/index.php'; + $_REQUEST['rest_route'] = '/wc/store/v1/cart'; + $request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' ); $request->set_header( 'X-WooPayments-Tokenized-Cart-Session-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ) ); $request->set_header( 'Content-Type', 'application/json' ); $session = new WC_Payments_Payment_Request_Session(); + $session->init(); - $response = $session->store_api_headers( new WP_REST_Response(), null, $request ); + // need to manually call this method, because otherwise WooCommerce hasn't instantiated the session when the request is made. + WC()->initialize_session(); + + $response = rest_do_request( $request ); + // manually calling 'rest_post_dispatch' because it is not called within the context of unit tests. + $response = apply_filters( 'rest_post_dispatch', $response, rest_get_server(), $request ); + $this->assertNotNull( apply_filters( 'woocommerce_session_handler', null ) ); $this->assertIsString( $response->get_headers()['X-WooPayments-Tokenized-Cart-Session'] ); } public function test_does_not_add_tokenized_session_headers_on_invalid_nonce() { - $_SERVER['REQUEST_URI'] = '/index.php'; - $_REQUEST['rest_route'] = '/wc/store/v1/cart'; - $request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' ); + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION_NONCE'] = 'invalid-nonce'; + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] = ''; + $_SERVER['REQUEST_URI'] = '/index.php'; + $_REQUEST['rest_route'] = '/wc/store/v1/cart'; + $request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' ); $request->set_header( 'X-WooPayments-Tokenized-Cart-Session-Nonce', 'invalid-nonce' ); $request->set_header( 'Content-Type', 'application/json' ); $session = new WC_Payments_Payment_Request_Session(); + $session->init(); - $response = $session->store_api_headers( new WP_REST_Response(), null, $request ); + // need to manually call this method, because otherwise WooCommerce hasn't instantiated the session when the request is made. + WC()->initialize_session(); + + $response = rest_do_request( $request ); + // manually calling 'rest_post_dispatch' because it is not called within the context of unit tests. + $response = apply_filters( 'rest_post_dispatch', $response, rest_get_server(), $request ); - $this->assertNotContains( 'X-WooPayments-Tokenized-Cart-Session', $response->get_headers() ); + $this->assertNull( apply_filters( 'woocommerce_session_handler', null ) ); + $this->assertNotContains( 'X-WooPayments-Tokenized-Cart-Session', array_keys( $response->get_headers() ) ); } public function test_does_not_use_custom_session_handler_on_invalid_nonce() { @@ -96,14 +114,50 @@ public function test_uses_custom_session_handler() { $session = new WC_Payments_Payment_Request_Session(); $session->init(); + // need to manually call this method, because otherwise WooCommerce hasn't instantiated the session when the request is made. + WC()->initialize_session(); + + WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 1 ); + WC()->cart->calculate_totals(); + + $this->assertCount( 1, WC()->cart->cart_contents ); + rest_do_request( $request ); $this->assertNotNull( apply_filters( 'woocommerce_session_handler', null ) ); + $this->assertInstanceOf( WC_Payments_Payment_Request_Session_Handler::class, WC()->session ); + // cart contents are not cleared. + $this->assertCount( 1, WC()->cart->cart_contents ); + } + + public function test_clears_cart_after_response_when_header_is_provided() { + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION_NONCE'] = wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ); + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] = ''; + $_SERVER['REQUEST_URI'] = '/index.php'; + $_REQUEST['rest_route'] = '/wc/store/v1/cart'; + $request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' ); + $request->set_header( 'X-WooPayments-Tokenized-Cart-Session-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ) ); + $request->set_header( 'X-WooPayments-Tokenized-Cart-Is-Ephemeral-Cart', '1' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $session = new WC_Payments_Payment_Request_Session(); + $session->init(); + // need to manually call this method, because otherwise WooCommerce hasn't instantiated the session when the request is made. WC()->initialize_session(); - $this->assertInstanceOf( WC_Payments_Payment_Request_Session_Handler::class, WC()->session ); + WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 1 ); + WC()->cart->calculate_totals(); + + $this->assertCount( 1, WC()->cart->cart_contents ); + + $response = rest_do_request( $request ); + // manually calling 'rest_post_dispatch' because it is not called within the context of unit tests. + apply_filters( 'rest_post_dispatch', $response, rest_get_server(), $request ); + + // cart contents are cleared, because the `X-WooPayments-Tokenized-Cart-Is-Ephemeral-Cart` header has been provided. + $this->assertCount( 0, WC()->cart->cart_contents ); } public function test_restores_cart_data_on_order_received_page() { diff --git a/tests/unit/test-class-wc-payments-webhook-processing-service.php b/tests/unit/test-class-wc-payments-webhook-processing-service.php index 74398f2cdd1..399e7d59173 100644 --- a/tests/unit/test-class-wc-payments-webhook-processing-service.php +++ b/tests/unit/test-class-wc-payments-webhook-processing-service.php @@ -9,6 +9,7 @@ use WCPay\Constants\Order_Status; use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Method; +use WCPay\Constants\Refund_Status; use WCPay\Database_Cache; use WCPay\Exceptions\Invalid_Payment_Method_Exception; use WCPay\Exceptions\Invalid_Webhook_Data_Exception; @@ -41,7 +42,7 @@ class WC_Payments_Webhook_Processing_Service_Test extends WCPAY_UnitTestCase { private $mock_remote_note_service; /** - * @var WC_Payments_Order_Service + * @var WC_Payments_Order_Service|MockObject */ private $order_service; @@ -98,7 +99,16 @@ public function set_up() { $this->order_service = $this->getMockBuilder( 'WC_Payments_Order_Service' ) ->setConstructorArgs( [ $this->createMock( WC_Payments_API_Client::class ) ] ) - ->setMethods( [ 'get_wcpay_refund_id_for_order', 'add_note_and_metadata_for_refund', 'create_refund_for_order', 'mark_terminal_payment_failed', 'handle_insufficient_balance_for_refund' ] ) + ->setMethods( + [ + 'get_wcpay_refund_id_for_order', + 'add_note_and_metadata_for_created_refund', + 'create_refund_for_order', + 'mark_terminal_payment_failed', + 'handle_insufficient_balance_for_refund', + 'handle_failed_refund', + ] + ) ->getMock(); $this->mock_db_wrapper = $this->getMockBuilder( WC_Payments_DB::class ) @@ -142,7 +152,6 @@ public function set_up() { ->expects( $this->any() ) ->method( 'get_id' ) ->willReturn( 1234 ); - WC_Payments::mode()->live(); } @@ -259,9 +268,9 @@ public function test_webhook_with_no_data_property() { } /** - * Test a valid refund sets failed meta. + * Test a failed refund without matched WC Refunds. */ - public function test_valid_failed_refund_webhook_sets_failed_meta() { + public function test_failed_refund_update_webhook_without_matched_wc_refund() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; @@ -273,36 +282,25 @@ public function test_valid_failed_refund_webhook_sets_failed_meta() { 'currency' => 'gbp', ]; - $this->mock_order->method( 'get_currency' )->willReturn( 'GBP' ); - - $this->mock_order - ->expects( $this->once() ) - ->method( 'add_order_note' ) - ->with( - 'A refund of £9.99 was unsuccessful using WooPayments (test_refund_id).' - ); - - // The expects condition here is the real test; we expect that the 'update_meta_data' function - // is called with the appropriate values. - $this->mock_order - ->expects( $this->once() ) - ->method( 'update_meta_data' ) - ->with( '_wcpay_refund_status', 'failed' ); - $this->mock_db_wrapper ->expects( $this->once() ) ->method( 'order_from_charge_id' ) ->with( 'test_charge_id' ) ->willReturn( $this->mock_order ); + $this->order_service + ->expects( $this->once() ) + ->method( 'handle_failed_refund' ) + ->with( $this->mock_order, 'test_refund_id', 999, 'gbp', null ); + // Run the test. $this->webhook_processing_service->process( $this->event_body ); } /** - * Test a vaild refund failure deletes WooCommerce Refund. + * Test a failed refund with matched WC Refunds. */ - public function test_valid_failed_refund_webhook_deletes_wc_refund() { + public function test_failed_refund_update_webhook_with_matched_wc_refund() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; @@ -340,130 +338,159 @@ public function test_valid_failed_refund_webhook_deletes_wc_refund() { ->with( 'test_charge_id' ) ->willReturn( $this->mock_order ); - $mock_refund_1 - ->expects( $this->never() ) - ->method( 'delete' ); - - $mock_refund_2 + $this->order_service ->expects( $this->once() ) - ->method( 'delete' ); + ->method( 'handle_failed_refund' ) + ->with( $this->mock_order, 'test_refund_id', 999, 'usd', $mock_refund_2 ); // Run the test. $this->webhook_processing_service->process( $this->event_body ); } /** - * Test a valid refund does not set failed meta. + * Test a refund update with status `cancelled` */ - public function test_non_failed_refund_update_webhook_does_not_set_failed_meta() { + public function test_cancelled_refund_update_webhook_with_matched_wc_refund() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; $this->event_body['data']['object'] = [ - 'status' => 'success', + 'status' => Refund_Status::CANCELED, + 'charge' => 'test_charge_id', + 'id' => 'test_refund_id', + 'amount' => 999, + 'currency' => 'usd', ]; + $mock_refund_1 = $this->createMock( WC_Order_Refund::class ); + $mock_refund_2 = $this->createMock( WC_Order_Refund::class ); + $this->order_service + ->expects( $this->exactly( 2 ) ) + ->method( 'get_wcpay_refund_id_for_order' ) + ->withConsecutive( + [ $mock_refund_1 ], + [ $mock_refund_2 ] + )->willReturnOnConsecutiveCalls( + 'another_test_refund_id', + 'test_refund_id' + ); + + $this->mock_order->method( 'get_refunds' )->willReturn( + [ + $mock_refund_1, + $mock_refund_2, + ] + ); + $this->mock_db_wrapper - ->expects( $this->never() ) - ->method( 'order_from_charge_id' ); + ->expects( $this->once() ) + ->method( 'order_from_charge_id' ) + ->with( 'test_charge_id' ) + ->willReturn( $this->mock_order ); - // The expects condition here is the real test; we expect that the 'update_meta_data' function - // is never called to update the meta data. - $this->mock_order - ->expects( $this->never() ) - ->method( 'update_meta_data' ); + $this->order_service + ->expects( $this->once() ) + ->method( 'handle_failed_refund' ) + ->with( $this->mock_order, 'test_refund_id', 999, 'usd', $mock_refund_2, true ); // Run the test. $this->webhook_processing_service->process( $this->event_body ); } /** - * Test a valid failed refund update webhook. + * Test a failed refund update webhook with insufficient funds. */ - public function test_valid_failed_refund_update_webhook() { + public function test_failed_refund_update_webhook_with_insufficient_funds() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; $this->event_body['data']['object'] = [ - 'status' => 'failed', - 'charge' => 'test_charge_id', - 'id' => 'test_refund_id', - 'amount' => 999, - 'currency' => 'gbp', + 'status' => 'failed', + 'charge' => 'charge_id', + 'id' => 'test_refund_id', + 'amount' => 999, + 'currency' => 'gbp', + 'failure_reason' => 'insufficient_funds', ]; - $this->mock_order->method( 'get_currency' )->willReturn( 'GBP' ); - - $this->mock_order - ->expects( $this->once() ) - ->method( 'add_order_note' ) - ->with( - 'A refund of £9.99 was unsuccessful using WooPayments (test_refund_id).' - ); - $this->mock_db_wrapper ->expects( $this->once() ) ->method( 'order_from_charge_id' ) - ->with( 'test_charge_id' ) + ->with( 'charge_id' ) ->willReturn( $this->mock_order ); - // Run the test. + $this->order_service + ->expects( $this->once() ) + ->method( 'handle_insufficient_balance_for_refund' ) + ->with( $this->mock_order, 999 ); + $this->webhook_processing_service->process( $this->event_body ); } /** - * Test a valid failed refund update webhook for non-USD. + * Test a refund does not set failed meta. */ - public function test_valid_failed_refund_update_webhook_non_usd() { + public function test_succeeded_refund_update_webhook_without_matched_wc_refund() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; $this->event_body['data']['object'] = [ - 'status' => 'failed', + 'status' => Refund_Status::SUCCEEDED, 'charge' => 'test_charge_id', 'id' => 'test_refund_id', 'amount' => 999, - 'currency' => 'eur', + 'currency' => 'usd', ]; - $this->mock_order->method( 'get_currency' )->willReturn( 'GBP' ); - - $this->mock_order - ->expects( $this->once() ) - ->method( 'add_order_note' ) - ->with( 'A refund of 9.99 was unsuccessful using WooPayments (test_refund_id).' ); - $this->mock_db_wrapper ->expects( $this->once() ) ->method( 'order_from_charge_id' ) ->with( 'test_charge_id' ) ->willReturn( $this->mock_order ); + $this->order_service + ->expects( $this->never() ) + ->method( 'add_note_and_metadata_for_created_refund' ); + // Run the test. $this->webhook_processing_service->process( $this->event_body ); } /** - * Test a valid failed refund update webhook for zero decimal currency. + * Test a valid failed refund update webhook. */ - public function test_valid_failed_refund_update_webhook_zero_decimal_currency() { + public function test_succeeded_refund_update_webhook_with_matched_wc_refund() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; $this->event_body['data']['object'] = [ - 'status' => 'failed', - 'charge' => 'test_charge_id', - 'id' => 'test_refund_id', - 'amount' => 999, - 'currency' => 'jpy', + 'status' => 'succeeded', + 'charge' => 'test_charge_id', + 'id' => 'test_refund_id', + 'amount' => 999, + 'currency' => 'usd', + 'balance_transaction' => 'txn_balance_transaction', ]; - $this->mock_order->method( 'get_currency' )->willReturn( 'GBP' ); + $mock_refund_1 = $this->createMock( WC_Order_Refund::class ); + $mock_refund_2 = $this->createMock( WC_Order_Refund::class ); + $this->order_service + ->expects( $this->exactly( 2 ) ) + ->method( 'get_wcpay_refund_id_for_order' ) + ->withConsecutive( + [ $mock_refund_1 ], + [ $mock_refund_2 ] + )->willReturnOnConsecutiveCalls( + 'another_test_refund_id', + 'test_refund_id' + ); - $this->mock_order - ->expects( $this->once() ) - ->method( 'add_order_note' ) - ->with( 'A refund of ¥999.00 was unsuccessful using WooPayments (test_refund_id).' ); + $this->mock_order->method( 'get_refunds' )->willReturn( + [ + $mock_refund_1, + $mock_refund_2, + ] + ); $this->mock_db_wrapper ->expects( $this->once() ) @@ -471,14 +498,19 @@ public function test_valid_failed_refund_update_webhook_zero_decimal_currency() ->with( 'test_charge_id' ) ->willReturn( $this->mock_order ); + $this->order_service + ->expects( $this->once() ) + ->method( 'add_note_and_metadata_for_created_refund' ) + ->with( $this->mock_order, $mock_refund_2, 'test_refund_id', 'txn_balance_transaction' ); + // Run the test. $this->webhook_processing_service->process( $this->event_body ); } /** - * Test a valid failed refund update webhook with an unknown charge ID. + * Test a failed refund update webhook with an unknown charge ID. */ - public function test_valid_failed_refund_update_webhook_with_unknown_charge_id() { + public function test_failed_refund_update_webhook_with_unknown_charge_id() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; @@ -504,53 +536,28 @@ public function test_valid_failed_refund_update_webhook_with_unknown_charge_id() } /** - * Test a valid failed refund update webhook with insufficient funds. + * Test an invalid status refund update webhook */ - public function test_valid_failed_refund_update_webhook_with_insufficient_funds() { + public function test_invalid_status_refund_update_webhook_throws_exceptions() { // Setup test request data. $this->event_body['type'] = 'charge.refund.updated'; $this->event_body['livemode'] = true; $this->event_body['data']['object'] = [ - 'status' => 'failed', - 'charge' => 'charge_id', - 'id' => 'test_refund_id', - 'amount' => 999, - 'currency' => 'gbp', - 'failure_reason' => 'insufficient_funds', + 'status' => 'invalid_status', + 'charge' => 'test_charge_id', + 'id' => 'test_refund_id', + 'amount' => 999, + 'currency' => 'gbp', ]; $this->mock_db_wrapper ->expects( $this->once() ) ->method( 'order_from_charge_id' ) - ->with( 'charge_id' ) + ->with( 'test_charge_id' ) ->willReturn( $this->mock_order ); - $this->order_service - ->expects( $this->once() ) - ->method( 'handle_insufficient_balance_for_refund' ) - ->with( $this->mock_order, 999 ); - - $this->webhook_processing_service->process( $this->event_body ); - } - - - /** - * Test a valid non-failed refund update webhook - */ - public function test_non_failed_refund_update_webhook() { - // Setup test request data. - $this->event_body['type'] = 'charge.refund.updated'; - $this->event_body['livemode'] = true; - $this->event_body['data']['object'] = [ - 'status' => 'updated', - 'charge' => 'test_charge_id', - 'id' => 'test_refund_id', - 'amount' => 999, - ]; - - $this->mock_db_wrapper - ->expects( $this->never() ) - ->method( 'order_from_charge_id' ); + $this->expectException( Invalid_Webhook_Data_Exception::class ); + $this->expectExceptionMessage( 'Invalid refund update status: invalid_status' ); // Run the test. $this->webhook_processing_service->process( $this->event_body ); @@ -739,6 +746,108 @@ public function test_payment_intent_successful_and_completes_order() { $this->webhook_processing_service->process( $this->event_body ); } + /** + * Tests that a payment_intent.succeeded event will add relevant metadata. + */ + public function test_payment_intent_successful_adds_relevant_metadata() { + $this->event_body['type'] = 'payment_intent.succeeded'; + $this->event_body['livemode'] = true; + $this->event_body['data']['object'] = [ + 'id' => $id = 'pi_123123123123123', // payment_intent's ID. + 'object' => 'payment_intent', + 'amount' => 1500, + 'charges' => [ + 'data' => [ + [ + 'id' => $charge_id = 'py_123123123123123', + 'payment_method' => $payment_method_id = 'pm_foo', + 'payment_method_details' => [ + 'type' => 'card', + ], + 'application_fee_amount' => 100, + ], + ], + ], + 'currency' => $currency = 'eur', + 'status' => $intent_status = Intent_Status::SUCCEEDED, + 'metadata' => [], + ]; + + $this->mock_api_client + ->expects( $this->once() ) + ->method( 'deserialize_payment_intention_object_from_array' ) + ->with( $this->event_body['data']['object'] ) + ->willReturn( + WC_Helper_Intention::create_intention( + [ + 'status' => $intent_status, + 'payment_method_options' => [ 'card' => [ 'request_three_d_secure' => 'automatic' ] ], + ] + ) + ); + + $this->mock_order + ->expects( $this->exactly( 7 ) ) + ->method( 'update_meta_data' ) + ->withConsecutive( + [ '_intent_id', $id ], + [ '_charge_id', $charge_id ], + [ '_payment_method_id', $payment_method_id ], + [ WC_Payments_Utils::ORDER_INTENT_CURRENCY_META_KEY, $currency ], + [ '_wcpay_transaction_fee', 1.0 ], + [ '_wcpay_net', 14.00 ], + [ '_intention_status', $intent_status ], + ); + + $this->mock_order + ->expects( $this->exactly( 2 ) ) + ->method( 'save' ); + + $this->mock_order + ->method( 'get_total' ) + ->willReturn( 15.00 ); + + $this->mock_order + ->expects( $this->exactly( 2 ) ) + ->method( 'has_status' ) + ->with( + [ + Order_Status::PROCESSING, + Order_Status::COMPLETED, + ] + ) + ->willReturn( false ); + + $this->mock_order + ->expects( $this->once() ) + ->method( 'payment_complete' ); + + $this->mock_db_wrapper + ->expects( $this->once() ) + ->method( 'order_from_intent_id' ) + ->with( 'pi_123123123123123' ) + ->willReturn( $this->mock_order ); + + $this->mock_order + ->method( 'get_data_store' ) + ->willReturn( new \WC_Mock_WC_Data_Store() ); + + $this->mock_order + ->method( 'get_meta' ) + ->willReturn( '' ); + + $this->mock_receipt_service + ->expects( $this->never() ) + ->method( 'send_customer_ipp_receipt_email' ); + + $this->mock_wcpay_gateway + ->expects( $this->never() ) + ->method( 'get_option' ); + + // Run the test. + $this->webhook_processing_service->process( $this->event_body ); + } + /** * Tests that a payment_intent.succeeded event will complete the order even if the intent was not properly attached into the order. */ @@ -1477,6 +1586,7 @@ public function test_process_full_refund_succeeded(): void { 'data' => [ [ 'id' => 'test_refund_id', + 'status' => Refund_Status::SUCCEEDED, 'amount' => 1800, 'currency' => 'usd', 'reason' => 'requested_by_customer', @@ -1518,7 +1628,7 @@ public function test_process_full_refund_succeeded(): void { $this->order_service ->expects( $this->once() ) - ->method( 'add_note_and_metadata_for_refund' ) + ->method( 'add_note_and_metadata_for_created_refund' ) ->with( $this->mock_order, $mock_refund, 'test_refund_id', 'txn_123' ); $this->webhook_processing_service->process( $this->event_body ); @@ -1533,6 +1643,7 @@ public function test_process_partial_refund_succeeded(): void { 'data' => [ [ 'id' => 'test_refund_id', + 'status' => Refund_Status::SUCCEEDED, 'amount' => 900, 'currency' => 'usd', 'reason' => 'requested_by_customer', @@ -1565,7 +1676,7 @@ public function test_process_partial_refund_succeeded(): void { $this->order_service ->expects( $this->once() ) - ->method( 'add_note_and_metadata_for_refund' ) + ->method( 'add_note_and_metadata_for_created_refund' ) ->with( $this->mock_order, $mock_refund, 'test_refund_id', 'txn_123' ); $this->webhook_processing_service->process( $this->event_body ); @@ -1626,7 +1737,7 @@ public function test_process_refund_ignores_processed_event(): void { $this->order_service ->expects( $this->never() ) - ->method( 'add_note_and_metadata_for_refund' ); + ->method( 'add_note_and_metadata_for_created_refund' ); $this->webhook_processing_service->process( $this->event_body ); } @@ -1640,7 +1751,7 @@ public function test_process_refund_ignores_event(): void { $this->order_service ->expects( $this->never() ) - ->method( 'add_note_and_metadata_for_refund' ); + ->method( 'add_note_and_metadata_for_created_refund' ); $this->webhook_processing_service->process( $this->event_body ); } @@ -1671,7 +1782,7 @@ public function test_process_refund_ignores_failed_refund_event(): void { $this->order_service ->expects( $this->never() ) - ->method( 'add_note_and_metadata_for_refund' ); + ->method( 'add_note_and_metadata_for_created_refund' ); } public function test_process_refund_throws_when_order_not_found(): void { diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php index 65fb97086b8..82a4bdffe19 100644 --- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php +++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php @@ -305,6 +305,7 @@ function ( $data ): bool { 'progressive' => false, 'collect_payout_requirements' => false, 'compatibility_data' => $this->get_mock_compatibility_data(), + 'referral_code' => null, ] ), true, @@ -436,42 +437,6 @@ public function test_get_currency_rates() { } - /** - * @dataProvider data_request_with_level3_data - */ - public function test_request_with_level3_data( $input_args, $expected_level3_args ) { - $this->mock_http_client - ->expects( $this->once() ) - ->method( 'remote_request' ) - ->with( - $this->anything(), - $this->callback( - function ( $request_args_json ) use ( $expected_level3_args ) { - $request_args = json_decode( $request_args_json, true ); - - $this->assertSame( $expected_level3_args, $request_args['level3'] ); - - return true; - } - ) - ) - ->willReturn( - [ - 'body' => wp_json_encode( [ 'result' => 'success' ] ), - 'response' => [ - 'code' => 200, - 'message' => 'OK', - ], - ] - ); - - PHPUnit_Utils::call_method( - $this->payments_api_client, - 'request_with_level3_data', - [ $input_args, 'intentions', 'POST' ] - ); - } - public function test_create_terminal_location_validation_array() { $this->expectException( API_Exception::class ); $this->expectExceptionMessageMatches( '~address.*required~i' ); @@ -568,70 +533,6 @@ public function test_delete_terminal_location_success() { ); } - /** - * Data provider for test_request_with_level3_data - */ - public function data_request_with_level3_data() { - return [ - 'australian_merchant' => [ - [ - 'level3' => [], - ], - [], - ], - 'american_merchant_no_line_items' => [ - [ - 'level3' => [ - 'merchant_reference' => 'abc123', - ], - ], - [ - 'merchant_reference' => 'abc123', - 'line_items' => [ - [ - 'discount_amount' => 0, - 'product_code' => 'empty-order', - 'product_description' => 'The order is empty', - 'quantity' => 1, - 'tax_amount' => 0, - 'unit_cost' => 0, - ], - ], - ], - ], - 'american_merchant_with_line_items' => [ - [ - 'level3' => [ - 'merchant_reference' => 'abc123', - 'line_items' => [ - [ - 'discount_amount' => 0, - 'product_code' => 'free-hug', - 'product_description' => 'Free hug', - 'quantity' => 1, - 'tax_amount' => 0, - 'unit_cost' => 0, - ], - ], - ], - ], - [ - 'merchant_reference' => 'abc123', - 'line_items' => [ - [ - 'discount_amount' => 0, - 'product_code' => 'free-hug', - 'product_description' => 'Free hug', - 'quantity' => 1, - 'tax_amount' => 0, - 'unit_cost' => 0, - ], - ], - ], - ], - ]; - } - /** * @dataProvider data_get_intent_description */ diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 16eee4a1da9..a65895983cc 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -8,10 +8,10 @@ * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 7.6 - * WC tested up to: 9.6.0 + * WC tested up to: 9.7.1 * Requires at least: 6.0 * Requires PHP: 7.3 - * Version: 9.1.0 + * Version: 9.2.0 * Requires Plugins: woocommerce * * @package WooCommerce\Payments
        -

        - -
        -
        -
        -
        -
        - +
        + + + +
        + + + ', $address_parts ) ); + ?> +
        + + + ', $contact_parts ) ); + ?> +
        + + +