diff --git a/README.md b/README.md index f43b580b083..4e3804e580c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ We currently support the following variables: ## Test account setup -For setting up a test account follow [these instructions](https://woo.com/document/woopayments/testing-and-troubleshooting/sandbox-mode/). +For setting up a test account follow [these instructions](https://woocommerce.com/document/woopayments/testing-and-troubleshooting/sandbox-mode/). You will need a externally accessible URL to set up the plugin. You can use ngrok for this. diff --git a/SECURITY.md b/SECURITY.md index aa165252b5a..88d7600d489 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ Generally, only the latest version of the extension has continued support. In s ## Reporting a Vulnerability -[WooPayments](https://woo.com/payments/) is an open-source plugin for WooCommerce. Our HackerOne program covers the plugin software. +[WooPayments](https://woocommerce.com/payments/) is an open-source plugin for WooCommerce. Our HackerOne program covers the plugin software. **For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.** diff --git a/assets/css/admin.css b/assets/css/admin.css index 9fbffb21907..baf66dcee88 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -67,6 +67,10 @@ background-image: url( '../images/cards/visa.svg' ); } +.payment-method__brand--cartes_bancaires { + background-image: url( '../images/cards/cartes_bancaires.svg' ); +} + .payment-method__brand--unknown { background-image: url( '../images/cards/unknown.svg' ); } diff --git a/assets/css/admin.rtl.css b/assets/css/admin.rtl.css index ea319683a6a..d43de4d4b7f 100644 --- a/assets/css/admin.rtl.css +++ b/assets/css/admin.rtl.css @@ -67,6 +67,10 @@ background-image: url( '../images/cards/visa.svg' ); } +.payment-method__brand--cartes_bancaires { + background-image: url( '../images/cards/cartes_bancaires.svg' ); +} + .payment-method__brand--unknown { background-image: url( '../images/cards/unknown.svg' ); } diff --git a/assets/images/bnpl_announcement_afterpay.png b/assets/images/bnpl_announcement_afterpay.png new file mode 100644 index 00000000000..ec2fd1666d0 Binary files /dev/null and b/assets/images/bnpl_announcement_afterpay.png differ diff --git a/assets/images/bnpl_announcement_clearpay.png b/assets/images/bnpl_announcement_clearpay.png new file mode 100644 index 00000000000..63ead5c0893 Binary files /dev/null and b/assets/images/bnpl_announcement_clearpay.png differ diff --git a/assets/images/cards/cartes_bancaires.svg b/assets/images/cards/cartes_bancaires.svg new file mode 100644 index 00000000000..94f51339e65 --- /dev/null +++ b/assets/images/cards/cartes_bancaires.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/cards/diners.svg b/assets/images/cards/diners.svg index fba5d197445..6e2242f0824 100644 --- a/assets/images/cards/diners.svg +++ b/assets/images/cards/diners.svg @@ -1,7 +1,19 @@ - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/assets/images/cards/discover.svg b/assets/images/cards/discover.svg index 629a039192d..356e32f75f2 100644 --- a/assets/images/cards/discover.svg +++ b/assets/images/cards/discover.svg @@ -1,45 +1,355 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-activity-empty-state.svg b/assets/images/payment-activity-empty-state.svg new file mode 100644 index 00000000000..e4699c8451b --- /dev/null +++ b/assets/images/payment-activity-empty-state.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/afterpay-logo.svg b/assets/images/payment-methods/afterpay-logo.svg index 2ba2f916d77..6ccb9083654 100644 --- a/assets/images/payment-methods/afterpay-logo.svg +++ b/assets/images/payment-methods/afterpay-logo.svg @@ -1 +1 @@ - + diff --git a/assets/images/payment-methods/link.svg b/assets/images/payment-methods/link.svg index 04a4d14086e..f782df4c383 100644 --- a/assets/images/payment-methods/link.svg +++ b/assets/images/payment-methods/link.svg @@ -1,4 +1,10 @@ - + + + + + + + diff --git a/bin/wcpay-live-branches/wcpay-live-branches.user.js b/bin/wcpay-live-branches/wcpay-live-branches.user.js index ebd09e6c75c..73df0f3abca 100644 --- a/bin/wcpay-live-branches/wcpay-live-branches.user.js +++ b/bin/wcpay-live-branches/wcpay-live-branches.user.js @@ -388,7 +388,7 @@ */ function appendHtml( el, contents ) { const $el = $( el ); - const wooColor = '#7F54B3'; // https://woo.com/brand-and-logo-guidelines/ + const wooColor = '#7F54B3'; // https://woocommerce.com/brand-and-logo-guidelines/ const styles = $( '
+ { +
+

+ { __( 'Buy now, pay later is here', 'woocommerce-payments' ) } +

+

+ { __( + // eslint-disable-next-line max-len + 'Boost conversions and give your shoppers additional buying power, with buy now, pay later — now available in your WooPayments dashboard.*', + 'woocommerce-payments' + ) } +

+

+ { __( + '*Subject to country availability', + 'woocommerce-payments' + ) } +

+ + ); +}; + +const container = document.getElementById( 'wpwrap' ); +if ( container ) { + const dialogWrapper = document.createElement( 'div' ); + container.appendChild( dialogWrapper ); + + ReactDOM.createRoot( dialogWrapper ).render( ); +} diff --git a/client/bnpl-announcement/style.scss b/client/bnpl-announcement/style.scss new file mode 100644 index 00000000000..4a6ca64e4cf --- /dev/null +++ b/client/bnpl-announcement/style.scss @@ -0,0 +1,68 @@ +.wcpay-bnpl-announcement { + &.wcpay-confirmation-modal.wcpay-confirmation-modal { + margin-top: auto; + height: auto; + + @media screen and ( min-width: 600px ) { + max-width: 400px; + } + + .components-modal__header { + padding: 0; + + .components-button.has-icon { + position: absolute; + top: 18px; + left: auto; + right: 18px; + } + } + + .components-modal__content { + padding: 0 20px 100px; + margin-top: 60px; + + @media screen and ( min-width: 600px ) { + padding: 0 35px 24px; + } + } + + .wcpay-confirmation-modal__separator { + opacity: 0; + } + } + + &__payment-icons { + display: flex; + justify-content: center; + align-items: flex-start; + flex-wrap: wrap; + gap: 17px; + margin-bottom: 20px; + + .payment-method__icon { + margin-right: 0; + max-height: 35px; + outline: none; + + &[alt='Affirm'] { + max-height: 30px; + } + } + } + + h1 { + text-align: left; + width: 100%; + } + + p { + text-align: left; + } + + .components-external-link { + padding: 6px 12px; + align-items: center; + display: flex; + } +} diff --git a/client/cart/blocks/index.js b/client/cart/blocks/index.js new file mode 100644 index 00000000000..bffe8b5e426 --- /dev/null +++ b/client/cart/blocks/index.js @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { renderBNPLCartMessaging } from './product-details'; +import { getUPEConfig } from 'wcpay/utils/checkout'; + +const { registerPlugin } = window.wp.plugins; + +const paymentMethods = getUPEConfig( 'paymentMethodsConfig' ); + +const BNPL_PAYMENT_METHODS = { + AFFIRM: 'affirm', + AFTERPAY: 'afterpay_clearpay', + KLARNA: 'klarna', +}; + +const bnplPaymentMethods = Object.values( BNPL_PAYMENT_METHODS ).filter( + ( method ) => method in paymentMethods +); + +if ( bnplPaymentMethods.length ) { + // Register BNPL site messaging on the cart block. + registerPlugin( 'bnpl-site-messaging', { + render: renderBNPLCartMessaging, + scope: 'woocommerce-checkout', + } ); +} diff --git a/client/cart/blocks/product-details.js b/client/cart/blocks/product-details.js new file mode 100644 index 00000000000..87e807d354e --- /dev/null +++ b/client/cart/blocks/product-details.js @@ -0,0 +1,117 @@ +/** + * External dependencies + */ +import { + Elements, + PaymentMethodMessagingElement, +} from '@stripe/react-stripe-js'; +import { select } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles'; +import { getUPEConfig } from 'utils/checkout'; +import WCPayAPI from '../../checkout/api'; +import request from '../../checkout/utils/request'; +import { useEffect, useState } from 'react'; + +// Create an API object, which will be used throughout the checkout. +const api = new WCPayAPI( + { + publishableKey: getUPEConfig( 'publishableKey' ), + accountId: getUPEConfig( 'accountId' ), + forceNetworkSavedCards: getUPEConfig( 'forceNetworkSavedCards' ), + locale: getUPEConfig( 'locale' ), + }, + request +); + +const isInEditor = () => { + const editorStore = select( 'core/editor' ); + + return !! editorStore; +}; + +// BNPL only supports 2 decimal places. +const normalizeAmount = ( amount, decimalPlaces = 2 ) => { + return amount * Math.pow( 10, 2 - decimalPlaces ); +}; + +const { ExperimentalOrderMeta } = window.wc.blocksCheckout; + +const ProductDetail = ( { cart, context } ) => { + const [ appearance, setAppearance ] = useState( + getUPEConfig( 'upeBnplCartBlockAppearance' ) || {} + ); + + const [ fontRules ] = useState( getFontRulesFromPage() ); + + useEffect( () => { + async function generateUPEAppearance() { + // Generate UPE input styles. + let upeAppearance = getAppearance( 'bnpl_cart_block' ); + upeAppearance = await api.saveUPEAppearance( + upeAppearance, + 'bnpl_cart_block' + ); + setAppearance( upeAppearance ); + } + + if ( Object.keys( appearance ).length === 0 ) { + generateUPEAppearance(); + } + }, [ appearance ] ); + + if ( Object.keys( appearance ).length === 0 ) { + return null; + } + + if ( context !== 'woocommerce/cart' ) { + return null; + } + + const cartTotal = normalizeAmount( + cart.cartTotals.total_price, + wcSettings.currency.precision + ); + + const { + country, + paymentMethods, + currencyCode, + } = window.wcpayStripeSiteMessaging; + + const amount = parseInt( cartTotal, 10 ) || 0; + + const options = { + amount: amount, + currency: currencyCode || 'USD', + paymentMethodTypes: paymentMethods || [], + countryCode: country, // Customer's country or base country of the store. + }; + + const stripe = api.getStripe(); + + return ( +
+ + + +
+ ); +}; + +export const renderBNPLCartMessaging = () => { + if ( isInEditor() ) { + return null; + } + return ( + + + + ); +}; diff --git a/client/cart/index.js b/client/cart/index.js index 7125abc99d8..3f995435df6 100644 --- a/client/cart/index.js +++ b/client/cart/index.js @@ -4,12 +4,13 @@ import { recordUserEvent } from 'tracks'; import { getConfig } from 'wcpay/utils/checkout'; import WooPayDirectCheckout from 'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout'; +import { shouldSkipWooPay } from 'wcpay/checkout/woopay/utils'; const recordProceedToCheckoutButtonClick = () => { recordUserEvent( 'wcpay_proceed_to_checkout_button_click', { - woopay_direct_checkout: Boolean( - getConfig( 'isWooPayDirectCheckoutEnabled' ) - ), + woopay_direct_checkout: + Boolean( getConfig( 'isWooPayDirectCheckoutEnabled' ) ) && + ! shouldSkipWooPay(), } ); }; diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 1e0a6187c21..869bed5a9a5 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -367,45 +367,6 @@ export default class WCPayAPI { } ); } - /** - * Confirm Stripe payment with fallback for rate limit error. - * - * @param {Object|StripeElements} elements Stripe elements. - * @param {Object} confirmParams Confirm payment request parameters. - * @param {string|null} paymentIntentSecret Payment intent secret used to validate payment on rate limit error - * - * @return {Promise} The payment confirmation promise. - */ - async handlePaymentConfirmation( - elements, - confirmParams, - paymentIntentSecret - ) { - const stripe = this.getStripe(); - const confirmPaymentResult = await stripe.confirmPayment( { - elements, - confirmParams, - } ); - if ( - paymentIntentSecret && - confirmPaymentResult.error && - confirmPaymentResult.error.code === 'lock_timeout' - ) { - const paymentIntentResult = await stripe.retrievePaymentIntent( - decryptClientSecret( paymentIntentSecret ) - ); - if ( - ! paymentIntentResult.error && - paymentIntentResult.paymentIntent.status === 'succeeded' - ) { - window.location.href = confirmParams.redirect_url; - return paymentIntentResult; //To prevent returning an error during the redirection. - } - } - - return confirmPaymentResult; - } - /** * Saves the calculated UPE appearance values in a transient. * @@ -572,4 +533,25 @@ export default class WCPayAPI { ...paymentData, } ); } + + /** + * Fetches the cart data from the woocommerce store api. + * + * @return {Object} JSON data. + * @throws Error if the response is not ok. + */ + pmmeGetCartData() { + return fetch( `${ getUPEConfig( 'storeApiURL' ) }/cart`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + } ).then( ( response ) => { + if ( ! response.ok ) { + throw new Error( response.statusText ); + } + return response.json(); + } ); + } } diff --git a/client/checkout/blocks/confirm-upe-payment.js b/client/checkout/blocks/confirm-upe-payment.js deleted file mode 100644 index 4746b37d3f9..00000000000 --- a/client/checkout/blocks/confirm-upe-payment.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Internal dependencies - */ -import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; - -/** - * Handles the confirmation of card payments (3DSv2 modals/SCA challenge). - * - * @param {WCPayAPI} api The API used for connection both with the server and Stripe. - * @param {string} redirectUrl The URL to redirect to after confirming the intent on Stripe. - * @param {boolean} paymentNeeded A boolean whether a payment or a setup confirmation is needed. - * @param {string|null} paymentIntentSecret Payment Intent Secret used to validate payment on rate limit error. - * @param {Object} elements Reference to the UPE elements mounted on the page. - * @param {Object} billingData An object containing the customer's billing data. - * @param {Object} shippingData An object containing the customer's shipping data, needed for Afterpay. - * @param {Object} emitResponse Various helpers for usage with observer response objects. - * @param {string} selectedUPEPaymentType The selected UPE payment type. - * @return {Object} An object, which contains the result from the action. - */ -export default async function confirmUPEPayment( - api, - redirectUrl, - paymentNeeded, - paymentIntentSecret, - elements, - billingData, - shippingData, - emitResponse, - selectedUPEPaymentType -) { - const name = - `${ billingData.first_name } ${ billingData.last_name }`.trim() || '-'; - - try { - const confirmParams = { - return_url: redirectUrl, - payment_method_data: { - billing_details: { - name, - email: - typeof billingData.email === 'string' - ? billingData.email.trim() - : '-', - phone: billingData.phone || '-', - address: { - country: billingData.country || '-', - postal_code: billingData.postcode || '-', - state: billingData.state || '-', - city: billingData.city || '-', - line1: billingData.address_1 || '-', - line2: billingData.address_2 || '-', - }, - }, - }, - }; - - // Afterpay requires shipping details to be passed. Not needed by other payment methods. - if ( PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY === selectedUPEPaymentType ) { - confirmParams.shipping = { - name: - `${ shippingData.shippingAddress.first_name } ${ shippingData.shippingAddress.last_name }`.trim() || - '-', - address: { - country: shippingData.shippingAddress.country || '_', - postal_code: shippingData.shippingAddress.postcode || '-', - state: shippingData.shippingAddress.state || '-', - city: shippingData.shippingAddress.city || '-', - line1: shippingData.shippingAddress.address_1 || '-', - line2: shippingData.shippingAddress.address_2 || '-', - }, - }; - } - - if ( paymentNeeded ) { - const { error } = await api.handlePaymentConfirmation( - elements, - confirmParams, - paymentIntentSecret - ); - if ( error ) { - throw error; - } - } else { - const { error } = await api.getStripe().confirmSetup( { - elements, - confirmParams, - } ); - if ( error ) { - throw error; - } - } - } catch ( error ) { - return { - type: 'error', - message: error.message, - messageContext: emitResponse.noticeContexts.PAYMENTS, - }; - } -} diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index b566fa4fbfa..5dd0bc9f0db 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -15,6 +15,7 @@ import { getUPEConfig } from 'utils/checkout'; import { isLinkEnabled } from '../utils/upe'; import WCPayAPI from '../api'; import { SavedTokenHandler } from './saved-token-handler'; +import PaymentMethodLabel from './payment-method-label'; import request from '../utils/request'; import enqueueFraudScripts from 'fraud-scripts'; import paymentRequestPaymentMethod from '../../payment-request/blocks'; @@ -68,6 +69,9 @@ const api = new WCPayAPI( }, request ); + +const stripeAppearance = getUPEConfig( 'wcBlocksUPEAppearance' ); + Object.entries( enabledPaymentMethodsConfig ) .filter( ( [ upeName ] ) => upeName !== 'link' ) .forEach( ( [ upeName, upeConfig ] ) => { @@ -99,19 +103,13 @@ Object.entries( enabledPaymentMethodsConfig ) paymentMethodId: upeMethods[ upeName ], // see .wc-block-checkout__payment-method styles in blocks/style.scss label: ( - <> - - { upeConfig.title } - { - - + ), ariaLabel: 'WooPayments', supports: { diff --git a/client/checkout/blocks/payment-method-label.js b/client/checkout/blocks/payment-method-label.js new file mode 100644 index 00000000000..35cb4e019fa --- /dev/null +++ b/client/checkout/blocks/payment-method-label.js @@ -0,0 +1,70 @@ +/** + * Internal dependencies + */ +import { + Elements, + PaymentMethodMessagingElement, +} from '@stripe/react-stripe-js'; +import { normalizeCurrencyToMinorUnit } from '../utils'; + +export default ( { + api, + upeConfig, + upeName, + stripeAppearance, + upeAppearanceTheme, +} ) => { + const cartData = wp.data.select( 'wc/store/cart' ).getCartData(); + const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; + + // Stripe expects the amount to be sent as the minor unit of 2 digits. + const amount = normalizeCurrencyToMinorUnit( + cartData.totals.total_price, + cartData.totals.currency_minor_unit + ); + + // Customer's country or base country of the store. + const currentCountry = + cartData.billingAddress.country || + window.wcBlocksCheckoutData.storeCountry; + + return ( + <> + + { upeConfig.title } + { bnplMethods.includes( upeName ) && + ( upeConfig.countries.length === 0 || + upeConfig.countries.includes( currentCountry ) ) && ( + <> + + + + + ) } + { + + + ); +}; diff --git a/client/checkout/blocks/payment-processor.js b/client/checkout/blocks/payment-processor.js index 2dec9a22ae8..694a9064e0a 100644 --- a/client/checkout/blocks/payment-processor.js +++ b/client/checkout/blocks/payment-processor.js @@ -19,11 +19,11 @@ import { useEffect, useRef } from 'react'; import { usePaymentCompleteHandler } from './hooks'; import { getStripeElementOptions, - useCustomerData, blocksShowLinkButtonHandler, getBlocksEmailValue, isLinkEnabled, } 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'; diff --git a/client/checkout/blocks/style.scss b/client/checkout/blocks/style.scss index fd4009d09de..daf09c07a85 100644 --- a/client/checkout/blocks/style.scss +++ b/client/checkout/blocks/style.scss @@ -49,20 +49,42 @@ button.wcpay-stripelink-modal-trigger:hover { span { width: 95%; - img { - float: right; + &:has( .StripeElement ) { + display: grid; + grid-template-columns: 1fr auto; } -} -#payment-method { - label img { + img { float: right; border: 0; padding: 0; - max-height: 1.618em; - min-height: 30px; + height: 24px; + max-height: 24px; + } + + .StripeElement { + width: 100%; + grid-column: 1 / span 2; + grid-row-start: 2; + pointer-events: none; + + + img { + grid-row: 1 / span 2; + grid-column: 2; + } } +} +#payment-method { + label.wc-block-components-radio-control__option-checked { + .StripeElement { + display: none; + } + img { + grid-column: 2; + grid-row: 1; + } + } /* stylelint-disable-next-line selector-id-pattern */ #radio-control-wc-payment-method-options-woocommerce_payments_affirm__label img { diff --git a/client/checkout/blocks/test/payment-processor.test.js b/client/checkout/blocks/test/payment-processor.test.js index c744c6d07b2..c94c7e432e9 100644 --- a/client/checkout/blocks/test/payment-processor.test.js +++ b/client/checkout/blocks/test/payment-processor.test.js @@ -12,8 +12,7 @@ import { PaymentElement } from '@stripe/react-stripe-js'; jest.mock( 'wcpay/checkout/classic/payment-processing', () => ( { validateElements: jest.fn().mockResolvedValue(), } ) ); -jest.mock( 'wcpay/checkout/utils/upe', () => ( { - ...jest.requireActual( 'wcpay/checkout/utils/upe' ), +jest.mock( 'wcpay/checkout/blocks/utils', () => ( { useCustomerData: jest.fn().mockReturnValue( { billingAddress: {} } ), } ) ); jest.mock( '../hooks', () => ( { diff --git a/client/checkout/blocks/utils.js b/client/checkout/blocks/utils.js new file mode 100644 index 00000000000..e23c1542825 --- /dev/null +++ b/client/checkout/blocks/utils.js @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { WC_STORE_CART } from 'wcpay/checkout/constants'; + +/** + * + * Custom React hook that provides customer data and related functions for managing customer information. + * The hook retrieves customer data from the WC_STORE_CART selector and dispatches actions to modify billing and shipping addresses. + * + * @return {Object} An object containing customer data and functions for managing customer information. + */ +export const useCustomerData = () => { + const customerData = useSelect( ( select ) => + select( WC_STORE_CART ).getCustomerData() + ); + const { + setShippingAddress, + setBillingData, + setBillingAddress, + } = useDispatch( WC_STORE_CART ); + + return { + // Backward compatibility billingData/billingAddress + billingAddress: customerData.billingAddress || customerData.billingData, + // Backward compatibility setBillingData/setBillingAddress + setBillingAddress: setBillingAddress || setBillingData, + setShippingAddress, + }; +}; diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index eb390660e78..156b73e1942 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -16,6 +16,7 @@ import { import { processPayment, mountStripePaymentElement, + mountStripePaymentMethodMessagingElement, renderTerms, createAndConfirmSetupIntent, maybeEnableStripeLink, @@ -69,6 +70,7 @@ jQuery( function ( $ ) { $( document.body ).on( 'updated_checkout', () => { maybeMountStripePaymentElement(); + injectStripePMMEContainers(); } ); $checkoutForm.on( generateCheckoutEventNames(), function () { @@ -150,6 +152,61 @@ jQuery( function ( $ ) { handleWooPayEmailInput( '#billing_email', api ); } + async function injectStripePMMEContainers() { + const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; + const labelBase = 'payment_method_woocommerce_payments_'; + const paymentMethods = getUPEConfig( 'paymentMethodsConfig' ); + const paymentMethodsKeys = Object.keys( paymentMethods ); + const cartData = await api.pmmeGetCartData(); + + for ( const method of paymentMethodsKeys ) { + if ( bnplMethods.includes( method ) ) { + const targetLabel = document.querySelector( + `label[for="${ labelBase }${ method }"]` + ); + const containerID = `stripe-pmme-container-${ method }`; + + if ( document.getElementById( containerID ) ) { + document.getElementById( containerID ).innerHTML = ''; + } + + if ( targetLabel ) { + let container = document.getElementById( containerID ); + if ( ! container ) { + container = document.createElement( 'span' ); + container.id = containerID; + container.dataset.paymentMethodType = method; + container.classList.add( 'stripe-pmme-container' ); + targetLabel.appendChild( container ); + } + + const currentCountry = + cartData?.billing_address?.country || + getUPEConfig( 'storeCountry' ); + + if ( + paymentMethods[ method ]?.countries.length === 0 || + paymentMethods[ method ]?.countries?.includes( + currentCountry + ) + ) { + await mountStripePaymentMethodMessagingElement( + api, + container, + { + amount: cartData?.totals?.total_price, + currency: cartData?.totals?.currency_code, + decimalPlaces: + cartData?.totals?.currency_minor_unit, + country: currentCountry, + } + ); + } + } + } + } + } + function processPaymentIfNotUsingSavedMethod( $form ) { const paymentMethodType = getSelectedUPEGatewayPaymentMethod(); if ( ! isUsingSavedPaymentMethod( paymentMethodType ) ) { @@ -216,10 +273,16 @@ jQuery( function ( $ ) { // We need to just find one field with missing information. If even only one is missing, just return early. return Boolean( - billingFieldsToValidate.find( - ( fieldName ) => - ! document.querySelector( `#${ fieldName }` )?.value - ) + billingFieldsToValidate.find( ( fieldName ) => { + const $field = document.querySelector( `#${ fieldName }` ); + const $formRow = $field.closest( '.form-row' ); + const isRequired = $formRow.classList.contains( + 'validate-required' + ); + const hasValue = $field?.value; + + return isRequired && ! hasValue; + } ) ); } } ); diff --git a/client/checkout/classic/payment-processing.js b/client/checkout/classic/payment-processing.js index 65f63b52176..704b0f1f97d 100644 --- a/client/checkout/classic/payment-processing.js +++ b/client/checkout/classic/payment-processing.js @@ -3,6 +3,7 @@ */ import { getUPEConfig } from 'wcpay/utils/checkout'; import { getAppearance, getFontRulesFromPage } from '../upe-styles'; +import { normalizeCurrencyToMinorUnit } from 'wcpay/checkout/utils'; import showErrorCheckout from 'wcpay/checkout/utils/show-error-checkout'; import { appendFingerprintInputToForm, @@ -401,6 +402,39 @@ export async function mountStripePaymentElement( api, domElement ) { upeElement.mount( domElement ); } +export async function mountStripePaymentMethodMessagingElement( + api, + domElement, + cartData +) { + const paymentMethodType = domElement.dataset.paymentMethodType; + const appearance = await initializeAppearance( api ); + + try { + const paymentMethodMessagingElement = api + .getStripe() + .elements( { + appearance: appearance, + fonts: getFontRulesFromPage(), + } ) + .create( 'paymentMethodMessaging', { + currency: cartData.currency, + amount: normalizeCurrencyToMinorUnit( + cartData.amount, + cartData.decimalPlaces + ), + countryCode: cartData.country, // Customer's country or base country of the store. + paymentMethodTypes: [ paymentMethodType ], + displayType: 'promotional_text', + } ); + + return paymentMethodMessagingElement.mount( domElement ); + } finally { + // Resolve the promise even if the element mounting fails. + return Promise.resolve(); + } +} + /** * Creates and confirms a setup intent using the provided ID, then appends the confirmed setup intent to the given jQuery form. * diff --git a/client/checkout/classic/style.scss b/client/checkout/classic/style.scss index 7ac3f9c5059..1b373153434 100644 --- a/client/checkout/classic/style.scss +++ b/client/checkout/classic/style.scss @@ -33,14 +33,62 @@ float: right; border: 0; padding: 0; - max-height: 1.618em; - min-height: 30px; + height: 24px !important; + max-height: 24px !important; + } +} + +li.wc_payment_method:has( .input-radio:not( :checked ) + + label + .stripe-pmme-container ) { + display: grid; + grid-template-columns: min-content 1fr; + grid-template-rows: auto auto; + align-items: baseline; + + .input-radio { + grid-row: 1; + grid-column: 1; + } + + label { + grid-column: 2; + grid-row: 1; + } + + img { + grid-row: 1 / span 2; + align-self: center; + } + + .stripe-pmme-container { + width: 100%; + grid-column: 1; + grid-row-start: 2; + pointer-events: none; } - .payment_method_woocommerce_payments_affirm label img { - min-width: 50px; + .payment_box { + flex: 0 0 100%; + grid-row: 2; + grid-column: 1 / span 2; } - .payment_method_woocommerce_payments_afterpay_clearpay label img { - min-width: 64px; +} + +li.wc_payment_method:has( .input-radio:checked + + label + .stripe-pmme-container ) { + display: block; + + .input-radio:checked { + + label { + .stripe-pmme-container { + display: none; + } + + img { + grid-column: 2; + } + } } } diff --git a/client/checkout/constants.js b/client/checkout/constants.js index e2e4c6dfe93..b2d4ac88fdc 100644 --- a/client/checkout/constants.js +++ b/client/checkout/constants.js @@ -11,7 +11,6 @@ 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_UPE = 'woocommerce_payments_upe'; export const PAYMENT_METHOD_NAME_PAYMENT_REQUEST = 'woocommerce_payments_payment_request'; export const PAYMENT_METHOD_NAME_WOOPAY_EXPRESS_CHECKOUT = diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js index 46e6e4ebc4e..255feaa3f55 100644 --- a/client/checkout/upe-styles/index.js +++ b/client/checkout/upe-styles/index.js @@ -74,6 +74,40 @@ const appearanceSelectors = { 'body', ], }, + bnplClassicCart: { + appendTarget: '.cart .quantity', + upeThemeInputSelector: '.cart .quantity .qty', + upeThemeLabelSelector: '.cart .quantity label', + rowElement: 'div', + validClasses: [ 'input-text' ], + invalidClasses: [ 'input-text', 'has-error' ], + backgroundSelectors: [ + '#payment-method-message', + '#main .entry-content .cart_totals', + '#main .entry-content', + '#main', + 'body', + ], + }, + bnplCartBlock: { + appendTarget: '.wc-block-cart .wc-block-components-quantity-selector', + upeThemeInputSelector: + '.wc-block-cart .wc-block-components-quantity-selector .wc-block-components-quantity-selector__input', + upeThemeLabelSelector: '.wc-block-components-text-input', + rowElement: 'div', + validClasses: [ 'wc-block-components-text-input' ], + invalidClasses: [ 'wc-block-components-text-input', 'has-error' ], + backgroundSelectors: [ + '.wc-block-components-bnpl-wrapper', + '.wc-block-components-order-meta', + '.wc-block-components-totals-wrapper', + '.wp-block-woocommerce-cart-order-summary-block', + '.wp-block-woocommerce-cart-totals-block', + '.wp-block-woocommerce-cart .wc-block-cart', + '.wp-block-woocommerce-cart', + 'body', + ], + }, /** * Update selectors to use alternate if not present on DOM. @@ -120,6 +154,12 @@ const appearanceSelectors = { case 'bnpl_product_page': appearanceSelector = this.bnplProductPage; break; + case 'bnpl_classic_cart': + appearanceSelector = this.bnplClassicCart; + break; + case 'bnpl_cart_block': + appearanceSelector = this.bnplCartBlock; + break; } return { diff --git a/client/checkout/utils/index.js b/client/checkout/utils/index.js new file mode 100644 index 00000000000..08f7c831754 --- /dev/null +++ b/client/checkout/utils/index.js @@ -0,0 +1,15 @@ +/** + * Normalizes the amount to the accuracy of the minor unit. + * + * @param {integer} amount The amount to normalize + * @param {integer} minorUnit The number of decimal places amount currently represents + * @param {integer} accuracy The number of decimal places to normalize to + * @return {integer} The normalized amount + */ +export const normalizeCurrencyToMinorUnit = ( + amount, + minorUnit = 2, + accuracy = 2 +) => { + return parseInt( amount * Math.pow( 10, accuracy - minorUnit ), 10 ); +}; diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js index 3773eeb5670..27876f1af5d 100644 --- a/client/checkout/utils/upe.js +++ b/client/checkout/utils/upe.js @@ -2,8 +2,7 @@ * Internal dependencies */ import { getUPEConfig } from 'wcpay/utils/checkout'; -import { WC_STORE_CART, getPaymentMethodsConstants } from '../constants'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { getPaymentMethodsConstants } from '../constants'; /** * Generates terms parameter for UPE, with value set for reusable payment methods @@ -183,32 +182,6 @@ export function dispatchChangeEventFor( element ) { element.dispatchEvent( event ); } -/** - * - * Custom React hook that provides customer data and related functions for managing customer information. - * The hook retrieves customer data from the WC_STORE_CART selector and dispatches actions to modify billing and shipping addresses. - * - * @return {Object} An object containing customer data and functions for managing customer information. - */ -export const useCustomerData = () => { - const customerData = useSelect( ( select ) => - select( WC_STORE_CART ).getCustomerData() - ); - const { - setShippingAddress, - setBillingData, - setBillingAddress, - } = useDispatch( WC_STORE_CART ); - - return { - // Backward compatibility billingData/billingAddress - billingAddress: customerData.billingAddress || customerData.billingData, - // Backward compatibility setBillingData/setBillingAddress - setBillingAddress: setBillingAddress || setBillingData, - setShippingAddress, - }; -}; - /** * Returns the prepared set of options needed to initialize the Stripe elements for UPE in Block Checkout. * The initial options have all the fields set to 'never' to hide them from the UPE, because all the diff --git a/client/checkout/woopay/connect/tests/woopay-connect-iframe.test.js b/client/checkout/woopay/connect/tests/woopay-connect-iframe.test.js new file mode 100644 index 00000000000..f99380eb88e --- /dev/null +++ b/client/checkout/woopay/connect/tests/woopay-connect-iframe.test.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { WooPayConnectIframe } from '../woopay-connect-iframe'; +import { getConfig } from 'wcpay/utils/checkout'; +import { getTracksIdentity } from 'tracks'; +import { + INJECTED_STATE, + setConnectIframeInjectedState, +} from 'wcpay/checkout/woopay/connect/connect-utils'; + +jest.mock( 'wcpay/utils/checkout', () => ( { + getConfig: jest.fn(), +} ) ); + +jest.mock( 'tracks', () => ( { + getTracksIdentity: jest.fn(), +} ) ); + +jest.mock( 'wcpay/checkout/woopay/connect/connect-utils', () => ( { + setConnectIframeInjectedState: jest.fn(), + INJECTED_STATE: { INJECTED: 'INJECTED' }, +} ) ); + +describe( 'WooPayConnectIframe', () => { + const mockTestMode = '1'; + const mockWoopayHost = 'https://woopay.test'; + const mockTracksUserId = '123'; + + beforeEach( () => { + getConfig.mockImplementation( ( key ) => { + if ( key === 'testMode' ) return mockTestMode; + if ( key === 'woopayHost' ) return mockWoopayHost; + } ); + + getTracksIdentity.mockResolvedValue( mockTracksUserId ); + + jest.clearAllMocks(); + } ); + + it( 'fetches configuration and sets iframe URL on mount', async () => { + const { container } = render( ); + + const iframe = container.querySelector( 'iframe' ); + await waitFor( () => { + expect( iframe.src ).toContain( mockWoopayHost ); + expect( iframe.src ).toContain( `testMode=${ mockTestMode }` ); + expect( iframe.src ).toContain( + `tracksUserIdentity=${ mockTracksUserId }` + ); + } ); + } ); + + it( 'sets up "postMessage" with success action on iframe load', async () => { + window.dispatchEvent = jest.fn(); + + let loadEventCallback; + const mockAddEventListener = jest.fn( ( event, callback ) => { + if ( event === 'load' ) { + loadEventCallback = callback; + } + } ); + + jest.spyOn( + HTMLIFrameElement.prototype, + 'addEventListener' + ).mockImplementation( mockAddEventListener ); + + render( ); + + // Simulate iframe load. + loadEventCallback(); + + await waitFor( () => { + expect( mockAddEventListener ).toHaveBeenCalledWith( + 'load', + expect.any( Function ) + ); + expect( setConnectIframeInjectedState ).toHaveBeenCalledWith( + INJECTED_STATE.INJECTED + ); + const messageEvent = window.dispatchEvent.mock.calls[ 0 ][ 0 ]; + expect( messageEvent.data.action ).toBe( + 'get_iframe_post_message_success' + ); + } ); + } ); +} ); diff --git a/client/checkout/woopay/direct-checkout/index.js b/client/checkout/woopay/direct-checkout/index.js index ef7fcc04388..b8795cfa3ea 100644 --- a/client/checkout/woopay/direct-checkout/index.js +++ b/client/checkout/woopay/direct-checkout/index.js @@ -11,11 +11,15 @@ import { debounce } from 'lodash'; import { WC_STORE_CART } from 'wcpay/checkout/constants'; import { waitMilliseconds } from 'wcpay/checkout/woopay/direct-checkout/utils'; import WooPayDirectCheckout from 'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout'; +import { shouldSkipWooPay } from 'wcpay/checkout/woopay/utils'; let isThirdPartyCookieEnabled = false; window.addEventListener( 'load', async () => { - if ( ! WooPayDirectCheckout.isWooPayDirectCheckoutEnabled() ) { + if ( + ! WooPayDirectCheckout.isWooPayDirectCheckoutEnabled() || + shouldSkipWooPay() + ) { return; } @@ -38,7 +42,10 @@ window.addEventListener( 'load', async () => { jQuery( ( $ ) => { $( document.body ).on( 'updated_cart_totals', async () => { - if ( ! WooPayDirectCheckout.isWooPayDirectCheckoutEnabled() ) { + if ( + ! WooPayDirectCheckout.isWooPayDirectCheckoutEnabled() || + shouldSkipWooPay() + ) { return; } diff --git a/client/checkout/woopay/direct-checkout/test/index.test.js b/client/checkout/woopay/direct-checkout/test/index.test.js new file mode 100644 index 00000000000..1cf516709cc --- /dev/null +++ b/client/checkout/woopay/direct-checkout/test/index.test.js @@ -0,0 +1,260 @@ +/* global $ */ +/** + * External dependencies + */ +import { fireEvent } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import WooPayDirectCheckout from 'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout'; + +const wpHookCallbacks = {}; + +jest.mock( '@wordpress/hooks', () => ( { + addAction: ( _hookName, _namespace, callback ) => { + wpHookCallbacks[ _hookName ] = callback; + }, +} ) ); + +jest.mock( + 'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout', + () => ( { + isWooPayDirectCheckoutEnabled: jest.fn(), + init: jest.fn(), + isWooPayThirdPartyCookiesEnabled: jest.fn(), + getCheckoutRedirectElements: jest.fn(), + isUserLoggedIn: jest.fn(), + maybePrefetchEncryptedSessionData: jest.fn(), + getClassicProceedToCheckoutButton: jest.fn(), + redirectToWooPay: jest.fn(), + setEncryptedSessionDataAsNotPrefetched: jest.fn(), + } ) +); + +let updatedCartTotalsCallback; +global.$ = jest.fn( () => ( { + on: ( event, callback ) => { + if ( event === 'updated_cart_totals' ) { + updatedCartTotalsCallback = callback; + } + }, + trigger: ( event ) => { + if ( event === 'updated_cart_totals' && updatedCartTotalsCallback ) { + updatedCartTotalsCallback(); + } + }, +} ) ); + +require( '../index.js' ); + +describe( 'WooPay direct checkout window "load" event listener', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'does not initialize WooPay direct checkout if not enabled', async () => { + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue( + false + ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + expect( + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.init ).not.toHaveBeenCalled(); + } ); + + it( 'calls `redirectToWooPay` method if third-party cookies are enabled and user is logged-in', async () => { + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue( + true + ); + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue( + true + ); + WooPayDirectCheckout.isUserLoggedIn.mockResolvedValue( true ); + WooPayDirectCheckout.getCheckoutRedirectElements.mockReturnValue( [] ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + expect( WooPayDirectCheckout.init ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.isUserLoggedIn ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.maybePrefetchEncryptedSessionData + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith( + expect.any( Array ), + true + ); + } ); + + it( 'calls `redirectToWooPay` method with "checkout_redirect" if third-party cookies are disabled', async () => { + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue( + true + ); + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue( + false + ); + WooPayDirectCheckout.getCheckoutRedirectElements.mockReturnValue( [] ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + expect( WooPayDirectCheckout.init ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.isUserLoggedIn ).not.toHaveBeenCalled(); + expect( + WooPayDirectCheckout.maybePrefetchEncryptedSessionData + ).not.toHaveBeenCalled(); + expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith( + expect.any( Array ), + false + ); + } ); +} ); + +describe( 'WooPay direct checkout "updated_cart_totals" jQuery event listener', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should not proceed if direct checkout is not enabled', async () => { + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue( + false + ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + $( document.body ).trigger( 'updated_cart_totals' ); + + expect( + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled + ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.getClassicProceedToCheckoutButton + ).not.toHaveBeenCalled(); + } ); + + it( 'calls `redirectToWooPay` method if third-party cookies are enabled and user is logged-in', async () => { + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue( + true + ); + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue( + true + ); + WooPayDirectCheckout.isUserLoggedIn.mockResolvedValue( true ); + WooPayDirectCheckout.getCheckoutRedirectElements.mockReturnValue( [] ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + await $( document.body ).trigger( 'updated_cart_totals' ); + + expect( WooPayDirectCheckout.init ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.isUserLoggedIn ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.maybePrefetchEncryptedSessionData + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith( + expect.any( Array ), + true + ); + } ); + + it( 'calls `redirectToWooPay` method with "checkout_redirect" if third-party cookies are disabled', async () => { + WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue( + true + ); + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue( + false + ); + WooPayDirectCheckout.getClassicProceedToCheckoutButton.mockReturnValue( + [] + ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + await $( document.body ).trigger( 'updated_cart_totals' ); + + expect( WooPayDirectCheckout.init ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.isUserLoggedIn ).not.toHaveBeenCalled(); + expect( + WooPayDirectCheckout.maybePrefetchEncryptedSessionData + ).not.toHaveBeenCalled(); + expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith( + expect.any( Array ), + false + ); + } ); +} ); + +describe( 'WooPay direct checkout cart item listeners', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should not prefetch encrypted session data on add item if third-party cookies are not enabled', async () => { + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue( + false + ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + await wpHookCallbacks[ + 'experimental__woocommerce_blocks-cart-add-item' + ](); + + expect( + WooPayDirectCheckout.setEncryptedSessionDataAsNotPrefetched + ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.maybePrefetchEncryptedSessionData + ).not.toHaveBeenCalled(); + } ); + + it( 'should prefetch encrypted session data on add item if third-party cookies are enabled and user is logged-in', async () => { + WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue( + true + ); + WooPayDirectCheckout.isUserLoggedIn.mockResolvedValue( true ); + + fireEvent.load( window ); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + await wpHookCallbacks[ + 'experimental__woocommerce_blocks-cart-add-item' + ](); + + expect( + WooPayDirectCheckout.maybePrefetchEncryptedSessionData + ).toHaveBeenCalled(); + expect( + WooPayDirectCheckout.setEncryptedSessionDataAsNotPrefetched + ).not.toHaveBeenCalled(); + } ); +} ); diff --git a/client/checkout/woopay/direct-checkout/test/woopay-direct-checkout.test.js b/client/checkout/woopay/direct-checkout/test/woopay-direct-checkout.test.js new file mode 100644 index 00000000000..c2f8fbfe147 --- /dev/null +++ b/client/checkout/woopay/direct-checkout/test/woopay-direct-checkout.test.js @@ -0,0 +1,130 @@ +/** + * Internal dependencies + */ +import WooPayDirectCheckout from '../woopay-direct-checkout'; + +describe( 'WooPayDirectCheckout', () => { + describe( 'redirectToWooPay', () => { + const originalLocation = window.location; + let elements; + + beforeEach( () => { + delete window.location; + window.location = { href: jest.fn() }; + + const checkoutButton = document.createElement( 'a' ); + checkoutButton.href = 'https://merchant.test/checkout'; + checkoutButton.classList.add( 'checkout-button' ); + checkoutButton.innerText = 'Proceed to checkout'; + + const divContainer = document.createElement( 'div' ); + divContainer.classList.add( 'wc-proceed-to-checkout' ); + divContainer.appendChild( checkoutButton ); + + document.body.appendChild( divContainer ); + + elements = document.querySelectorAll( + WooPayDirectCheckout.redirectElements + .CLASSIC_CART_PROCEED_BUTTON + ); + + WooPayDirectCheckout.teardown = jest.fn(); + WooPayDirectCheckout.getWooPayCheckoutUrl = jest.fn(); + WooPayDirectCheckout.getWooPayMinimumSessionUrl = jest.fn(); + } ); + + afterEach( () => { + window.location = originalLocation; + elements.forEach( ( el ) => el.parentElement.remove() ); + jest.clearAllMocks(); + } ); + + it( 'should add event listeners to provided "proceed to checkout" button elements', () => { + elements.forEach( ( element ) => { + element.addEventListener = jest.fn(); + } ); + + WooPayDirectCheckout.redirectToWooPay( elements ); + + elements.forEach( ( element ) => { + expect( element.addEventListener ).toHaveBeenCalledWith( + 'click', + expect.any( Function ) + ); + } ); + } ); + + it( 'should add loading spinner when shortcode cart button is clicked', () => { + WooPayDirectCheckout.redirectToWooPay( elements, false ); + + elements[ 0 ].click(); + + expect( + elements[ 0 ].querySelector( + 'span.wc-block-components-spinner' + ) + ).not.toBeNull(); + } ); + + it( 'should redirect not logged in user to WooPay minimum session URL', async () => { + WooPayDirectCheckout.getWooPayMinimumSessionUrl.mockResolvedValue( + 'https://woopay.test/woopay?checkout_redirect=1&blog_id=1&session=1&iv=1&hash=1' + ); + + WooPayDirectCheckout.redirectToWooPay( elements, false ); + + elements[ 0 ].click(); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + expect( + WooPayDirectCheckout.getWooPayMinimumSessionUrl + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.teardown ).toHaveBeenCalled(); + expect( window.location.href ).toBe( + 'https://woopay.test/woopay?checkout_redirect=1&blog_id=1&session=1&iv=1&hash=1' + ); + } ); + + it( 'should redirect logged in user to WooPay checkout URL', async () => { + WooPayDirectCheckout.getWooPayCheckoutUrl.mockResolvedValue( + 'https://woopay.test/woopay?platform_checkout_key=1234567890' + ); + + WooPayDirectCheckout.redirectToWooPay( elements, true ); + + elements[ 0 ].click(); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + expect( + WooPayDirectCheckout.getWooPayCheckoutUrl + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.teardown ).toHaveBeenCalled(); + expect( window.location.href ).toBe( + 'https://woopay.test/woopay?platform_checkout_key=1234567890' + ); + } ); + + it( 'should redirect to merchant checkout if WooPay checkout URL is not available', async () => { + // Throw an error to simulate a failure in getting the WooPay checkout URL. + WooPayDirectCheckout.getWooPayCheckoutUrl.mockRejectedValue( + new Error( 'Could not retrieve WooPay checkout URL.' ) + ); + + WooPayDirectCheckout.redirectToWooPay( elements, true ); + + elements[ 0 ].click(); + + await new Promise( ( resolve ) => setImmediate( resolve ) ); + + expect( + WooPayDirectCheckout.getWooPayCheckoutUrl + ).toHaveBeenCalled(); + expect( WooPayDirectCheckout.teardown ).toHaveBeenCalled(); + expect( window.location.href ).toBe( + 'https://merchant.test/checkout' + ); + } ); + } ); +} ); diff --git a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js index 40321a32545..6bd5ee33d34 100644 --- a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js +++ b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js @@ -249,6 +249,11 @@ class WooPayDirectCheckout { spinner.classList.add( 'wc-block-components-spinner' ); spinner.style.position = 'relative'; spinner.style.fontSize = 'unset'; + spinner.style.display = 'inline'; + spinner.style.lineHeight = '0'; + spinner.style.margin = '0'; + spinner.style.border = '0'; + spinner.style.padding = '0'; // Remove the existing content of the button. // Set innerHTML to ' ' to keep the button's height. element.innerHTML = ' '; diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index ce3040387e4..74ddd397bf3 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -10,8 +10,9 @@ import { getTargetElement, validateEmail, appendRedirectionParams, + shouldSkipWooPay, + deleteSkipWooPayCookie, } from './utils'; -import { select } from '@wordpress/data'; export const handleWooPayEmailInput = async ( field, @@ -75,11 +76,19 @@ export const handleWooPayEmailInput = async ( //Checks if customer has clicked the back button to prevent auto redirect const searchParams = new URLSearchParams( window.location.search ); + const isSkipWoopayCookieSet = shouldSkipWooPay(); const customerClickedBackButton = ( typeof performance !== 'undefined' && performance.getEntriesByType( 'navigation' )[ 0 ].type === 'back_forward' ) || - searchParams.get( 'skip_woopay' ) === 'true'; + searchParams.get( 'skip_woopay' ) === 'true' || + isSkipWoopayCookieSet; // We enforce and extend the skipping to the entire user session. + + if ( customerClickedBackButton && ! isSkipWoopayCookieSet ) { + const now = new Date(); + const followingDay = new Date( now.getTime() + 24 * 60 * 60 * 1000 ); // 24 hours later + document.cookie = `skip_woopay=1; path=/; expires=${ followingDay.toUTCString() }`; + } // Track the current state of the header. This default // value should match the default state on the platform. @@ -539,6 +548,7 @@ export const handleWooPayEmailInput = async ( break; case 'redirect_to_woopay_skip_session_init': if ( e.data.redirectUrl ) { + deleteSkipWooPayCookie(); window.location = appendRedirectionParams( e.data.redirectUrl ); @@ -558,6 +568,7 @@ export const handleWooPayEmailInput = async ( return; } if ( response.result === 'success' ) { + deleteSkipWooPayCookie(); window.location = response.url; } else { showErrorMessage(); @@ -617,13 +628,14 @@ export const handleWooPayEmailInput = async ( } ); if ( ! customerClickedBackButton ) { - const paymentMethods = await select( - 'wc/store/payment' - ).getAvailablePaymentMethods(); - - const hasWCPayPaymentMethod = paymentMethods.hasOwnProperty( - 'woocommerce_payments' + const hasWcPayElementOnBlocks = document.getElementById( + 'radio-control-wc-payment-method-options-woocommerce_payments' + ); + const hasWcPayElementOnShortcode = document.getElementById( + 'payment_method_woocommerce_payments' ); + const hasWCPayPaymentMethod = + hasWcPayElementOnBlocks || hasWcPayElementOnShortcode; // Check if user already has a WooPay login session and only open the iframe if there is WCPay. if ( @@ -639,7 +651,7 @@ export const handleWooPayEmailInput = async ( dispatchUserExistEvent( true ); }, 2000 ); - recordUserEvent( 'woopay_skipped', {}, true ); + recordUserEvent( 'woopay_skipped', {} ); searchParams.delete( 'skip_woopay' ); diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index 893e3ca545b..2220a1422e6 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -16,7 +16,10 @@ import { recordUserEvent } from 'tracks'; import { getConfig } from 'wcpay/utils/checkout'; import { showErrorMessage } from 'wcpay/checkout/woopay/express-button/utils'; import interpolateComponents from '@automattic/interpolate-components'; -import { appendRedirectionParams } from 'wcpay/checkout/woopay/utils'; +import { + appendRedirectionParams, + deleteSkipWooPayCookie, +} from 'wcpay/checkout/woopay/utils'; import WooPayFirstPartyAuth from 'wcpay/checkout/woopay/express-button/woopay-first-party-auth'; const BUTTON_WIDTH_THRESHOLD = 140; @@ -135,6 +138,8 @@ export const WoopayExpressCheckoutButton = ( { source: context, } ); + deleteSkipWooPayCookie(); + if ( ! canAddProductToCart() ) { return; } @@ -183,6 +188,8 @@ export const WoopayExpressCheckoutButton = ( { source: context, } ); + deleteSkipWooPayCookie(); + if ( ! canAddProductToCart() ) { return; } diff --git a/client/checkout/woopay/test/utils.test.js b/client/checkout/woopay/test/utils.test.js new file mode 100644 index 00000000000..d07f5b6a0bd --- /dev/null +++ b/client/checkout/woopay/test/utils.test.js @@ -0,0 +1,59 @@ +/** + * Internal dependencies + */ +import { shouldSkipWooPay } from 'wcpay/checkout/woopay/utils'; + +describe( 'WooPay Utils', () => { + const originalDocumentCookie = window.document.cookie; + + afterEach( () => { + Object.defineProperty( window.document, 'cookie', { + writable: true, + value: originalDocumentCookie, + } ); + } ); + + test( 'should skip WooPay returns true if cookie is set', () => { + Object.defineProperty( window.document, 'cookie', { + writable: true, + value: 'skip_woopay=1', + } ); + + const shouldSkip = shouldSkipWooPay(); + + expect( shouldSkip ).toBe( true ); + } ); + + test( 'should skip WooPay returns false if cookie is not set', () => { + Object.defineProperty( window.document, 'cookie', { + writable: true, + value: 'something=else', + } ); + + const shouldSkip = shouldSkipWooPay(); + + expect( shouldSkip ).toBe( false ); + } ); + + test( 'should not skip WooPay if skip_woopay cookie is set to 10', () => { + Object.defineProperty( window.document, 'cookie', { + writable: true, + value: 'skip_woopay=10', + } ); + + const shouldSkip = shouldSkipWooPay(); + + expect( shouldSkip ).toBe( false ); + } ); + + test( 'should not skip WooPay if skip_woopay cookie is called something else', () => { + Object.defineProperty( window.document, 'cookie', { + writable: true, + value: 'sskip_woopay=1', + } ); + + const shouldSkip = shouldSkipWooPay(); + + expect( shouldSkip ).toBe( false ); + } ); +} ); diff --git a/client/checkout/woopay/utils.js b/client/checkout/woopay/utils.js index a7b9a3a6152..f86d3973a7a 100644 --- a/client/checkout/woopay/utils.js +++ b/client/checkout/woopay/utils.js @@ -58,3 +58,39 @@ export const appendRedirectionParams = ( woopayUrl ) => { return url.href; }; + +/** + * Checks if a session cookie is set in order to determine if the user has opted to skip WooPay. + * + * @return {boolean} True if the user has opted to skip WooPay. + */ +export const shouldSkipWooPay = () => { + const cookies = document.cookie.split( ';' ); + const skipWooPayCookie = cookies.find( ( cookie ) => + cookie.includes( 'skip_woopay' ) + ); + + if ( ! skipWooPayCookie ) { + return false; + } + + const skipWooPayCookieSplit = skipWooPayCookie?.split( '=' ); + + return ( + skipWooPayCookieSplit[ 0 ].trim() === 'skip_woopay' && + skipWooPayCookieSplit[ 1 ].trim() === '1' + ); +}; + +/** + * Deletes the skip_woopay cookie. + * This should be called when the user explicitly opts to pay with WooPay. + */ +export const deleteSkipWooPayCookie = () => { + if ( ! shouldSkipWooPay() ) { + return; + } + + document.cookie = + 'skip_woopay=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;'; +}; diff --git a/client/components/account-balances/strings.ts b/client/components/account-balances/strings.ts index 04295427dd2..46d211f274b 100644 --- a/client/components/account-balances/strings.ts +++ b/client/components/account-balances/strings.ts @@ -26,7 +26,7 @@ export const fundLabelStrings = { export const documentationUrls = { depositSchedule: - 'https://woo.com/document/woopayments/deposits/deposit-schedule/', + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/', negativeBalance: - 'https://woo.com/document/woopayments/fees-and-debits/account-showing-negative-balance/', + 'https://woocommerce.com/document/woopayments/fees-and-debits/account-showing-negative-balance/', }; diff --git a/client/components/account-balances/test/index.test.tsx b/client/components/account-balances/test/index.test.tsx index 42de7f6710f..6c277094ae5 100644 --- a/client/components/account-balances/test/index.test.tsx +++ b/client/components/account-balances/test/index.test.tsx @@ -326,7 +326,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getByRole( 'link' ) ).toHaveAttribute( 'href', - 'https://woo.com/document/woopayments/deposits/deposit-schedule/' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/' ); } ); @@ -345,7 +345,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getAllByRole( 'link' )[ 1 ] ).toHaveAttribute( 'href', - 'https://woo.com/document/woopayments/fees-and-debits/account-showing-negative-balance/' + 'https://woocommerce.com/document/woopayments/fees-and-debits/account-showing-negative-balance/' ); } ); @@ -364,7 +364,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getAllByRole( 'link' )[ 1 ] ).toHaveAttribute( 'href', - 'https://woo.com/document/woopayments/fees-and-debits/account-showing-negative-balance/' + 'https://woocommerce.com/document/woopayments/fees-and-debits/account-showing-negative-balance/' ); } ); @@ -384,7 +384,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getByRole( 'link' ) ).toHaveAttribute( 'href', - 'https://woo.com/document/woopayments/deposits/deposit-schedule/' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/' ); } ); diff --git a/client/components/account-status/account-tools/index.tsx b/client/components/account-status/account-tools/index.tsx index ba5b916d123..9e2b255a6b5 100644 --- a/client/components/account-status/account-tools/index.tsx +++ b/client/components/account-status/account-tools/index.tsx @@ -12,9 +12,11 @@ import strings from './strings'; import './styles.scss'; import ResetAccountModal from 'wcpay/overview/modal/reset-account'; import { trackAccountReset } from 'wcpay/onboarding/tracking'; +import { recordEvent } from 'wcpay/tracks'; interface Props { accountLink: string; + detailsSubmitted: boolean; openModal: () => void; } @@ -28,6 +30,7 @@ const handleReset = () => { export const AccountTools: React.FC< Props > = ( props: Props ) => { const accountLink = props.accountLink; + const detailsSubmitted = props.detailsSubmitted; const [ modalVisible, setModalVisible ] = useState( false ); return ( @@ -38,15 +41,26 @@ export const AccountTools: React.FC< Props > = ( props: Props ) => {

{ strings.description }

{ /* Use wrapping div to keep buttons grouped together. */ }
+ { ! detailsSubmitted && ( + + ) } -
`; + +exports[`AccountTools should render in sandbox mode 1`] = ` +
+ +
+`; diff --git a/client/components/account-status/account-tools/test/index.test.tsx b/client/components/account-status/account-tools/test/index.test.tsx index 295970da639..d2362050cbe 100644 --- a/client/components/account-status/account-tools/test/index.test.tsx +++ b/client/components/account-status/account-tools/test/index.test.tsx @@ -26,24 +26,44 @@ describe( 'AccountTools', () => { }; const { container } = render( - + ); expect( container ).toMatchSnapshot(); } ); - it( 'should not render in sandbox mode', () => { + it( 'should render in sandbox mode', () => { + global.wcpaySettings = { + devMode: true, + }; + + const { container } = render( + + ); + + expect( container ).toMatchSnapshot(); + } ); + + it( 'should render in sandbox mode for details submitted account without finish setup button', () => { global.wcpaySettings = { devMode: true, }; render( - + ); - expect( - screen.queryByText( - 'If you are experiencing problems completing account setup, or need to change the email/country associated with your account, you can reset your account and start from the beginning.' - ) - ).not.toBeInTheDocument(); + expect( screen.queryByText( 'Finish setup' ) ).not.toBeInTheDocument(); } ); } ); diff --git a/client/components/account-status/index.js b/client/components/account-status/index.js index 9a54d660951..581d494b415 100755 --- a/client/components/account-status/index.js +++ b/client/components/account-status/index.js @@ -25,6 +25,7 @@ import './style.scss'; import './shared.scss'; import { AccountTools } from './account-tools'; import { isInDevMode } from 'wcpay/utils'; +import { recordEvent } from 'wcpay/tracks'; const AccountStatusCard = ( props ) => { const { title, children, value } = props; @@ -59,6 +60,7 @@ const AccountStatusError = () => { const AccountStatusDetails = ( props ) => { const { accountStatus, accountFees } = props; + const cardTitle = ( <> @@ -74,7 +76,16 @@ const AccountStatusDetails = ( props ) => { /> - @@ -105,7 +116,10 @@ const AccountStatusDetails = ( props ) => { /> { ( ! accountStatus.detailsSubmitted || isInDevMode() ) && ( - + ) } { accountFees.length > 0 && ( diff --git a/client/components/deposits-overview/deposit-notices.tsx b/client/components/deposits-overview/deposit-notices.tsx index 6a77fe5a6ab..9ce039a089e 100644 --- a/client/components/deposits-overview/deposit-notices.tsx +++ b/client/components/deposits-overview/deposit-notices.tsx @@ -12,6 +12,7 @@ import { ExternalLink } from '@wordpress/components'; * Internal dependencies */ import InlineNotice from 'components/inline-notice'; +import { recordEvent } from 'wcpay/tracks'; /** * Renders a notice informing the user that their deposits are suspended. @@ -35,7 +36,7 @@ export const SuspendedDepositNotice: React.FC = () => { suspendLink: ( ), @@ -61,7 +62,7 @@ export const DepositIncludesLoanPayoutNotice: React.FC = () => ( // eslint-disable-next-line jsx-a11y/anchor-has-content ( ), }, @@ -144,7 +145,7 @@ export const NegativeBalanceDepositsPausedNotice: React.FC = () => ( ), }, @@ -179,7 +180,7 @@ export const DepositMinimumBalanceNotice: React.FC< { ), }, @@ -205,7 +206,7 @@ export const NoFundsAvailableForDepositNotice: React.FC = () => ( ), }, @@ -234,7 +235,16 @@ export const DepositFailureNotice: React.FC< { 'woocommerce-payments' ), components: { - updateLink: , + updateLink: ( + + recordEvent( 'wcpay_account_details_link_clicked', { + source: 'deposit-notices', + } ) + } + href={ updateAccountLink } + /> + ), }, } ) } diff --git a/client/components/deposits-overview/deposit-schedule.tsx b/client/components/deposits-overview/deposit-schedule.tsx index eed43a4ae81..f589bf1ad12 100644 --- a/client/components/deposits-overview/deposit-schedule.tsx +++ b/client/components/deposits-overview/deposit-schedule.tsx @@ -136,7 +136,7 @@ const DepositSchedule: React.FC< DepositScheduleProps > = ( { rel="external noopener noreferrer" target="_blank" href={ - 'https://woo.com/document/woopayments/deposits/deposit-schedule/#pending-period-chart' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/#pending-period-chart' } /> ), @@ -156,7 +156,7 @@ const DepositSchedule: React.FC< DepositScheduleProps > = ( { rel="external noopener noreferrer" target="_blank" href={ - 'https://woo.com/document/woopayments/deposits/deposit-schedule/#available-funds' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/#available-funds' } /> ), @@ -176,7 +176,7 @@ const DepositSchedule: React.FC< DepositScheduleProps > = ( { rel="external noopener noreferrer" target="_blank" href={ - 'https://woo.com/document/woopayments/deposits/change-deposit-schedule/' + 'https://woocommerce.com/document/woopayments/deposits/change-deposit-schedule/' } /> ), diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index 8c0801d8e02..fdf056efad0 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -414,7 +414,7 @@ exports[`Suspended Deposit Notice Renders Component Renders 1`] = ` . Learn more diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index 6cdaaeade73..4853aff7bf5 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -366,7 +366,7 @@ describe( 'Deposits Overview information', () => { } ) ).toHaveAttribute( 'href', - 'https://woo.com/document/woopayments/stripe-capital/overview/' + 'https://woocommerce.com/document/woopayments/stripe-capital/overview/' ); } ); @@ -438,7 +438,7 @@ describe( 'Deposits Overview information', () => { } ); expect( getByRole( 'link', { name: /Why\?/ } ) ).toHaveAttribute( 'href', - 'https://woo.com/document/woopayments/deposits/deposit-schedule/#new-accounts' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/#new-accounts' ); } ); } ); diff --git a/client/components/deposits-status/index.tsx b/client/components/deposits-status/index.tsx index 635c2326afc..b5c61b2cf1c 100644 --- a/client/components/deposits-status/index.tsx +++ b/client/components/deposits-status/index.tsx @@ -63,7 +63,7 @@ const DepositsStatusSuspended: React.FC< DepositsStatusProps > = ( props ) => { const { iconSize } = props; const learnMoreHref = - 'https://woo.com/document/woopayments/deposits/why-deposits-suspended/'; + 'https://woocommerce.com/document/woopayments/deposits/why-deposits-suspended/'; const description = createInterpolateElement( /* translators: - suspended accounts FAQ URL */ diff --git a/client/components/deposits-status/test/__snapshots__/index.js.snap b/client/components/deposits-status/test/__snapshots__/index.js.snap index 41ec2b3b6c9..9d92c7cd823 100644 --- a/client/components/deposits-status/test/__snapshots__/index.js.snap +++ b/client/components/deposits-status/test/__snapshots__/index.js.snap @@ -20,7 +20,7 @@ exports[`DepositsStatus renders blocked status 1`] = ` Temporarily suspended ( @@ -51,7 +51,7 @@ exports[`DepositsStatus renders blocked status 2`] = ` Temporarily suspended ( @@ -174,7 +174,7 @@ exports[`DepositsStatus renders pending verification status 1`] = ` Temporarily suspended ( diff --git a/client/components/payment-method-details/index.js b/client/components/payment-method-details/index.js index d47e3cd3086..a1f447d4672 100755 --- a/client/components/payment-method-details/index.js +++ b/client/components/payment-method-details/index.js @@ -59,10 +59,14 @@ const PaymentMethodDetails = ( props ) => { return ; } - const brand = - paymentMethod && paymentMethod.brand - ? paymentMethod.brand - : payment.type; + let brand = payment.type; + if ( paymentMethod && paymentMethod.brand ) { + brand = paymentMethod.brand; + } + if ( paymentMethod && paymentMethod.network ) { + brand = paymentMethod.network; + } + const details = formatDetails( payment ); return ( diff --git a/client/components/payment-method-disabled-tooltip/index.tsx b/client/components/payment-method-disabled-tooltip/index.tsx index 10dc92f857e..a2b169bf98c 100644 --- a/client/components/payment-method-disabled-tooltip/index.tsx +++ b/client/components/payment-method-disabled-tooltip/index.tsx @@ -14,9 +14,9 @@ import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; export const DocumentationUrlForDisabledPaymentMethod = { DEFAULT: - 'https://woo.com/document/woopayments/payment-methods/additional-payment-methods/#method-cant-be-enabled', + 'https://woocommerce.com/document/woopayments/payment-methods/additional-payment-methods/#method-cant-be-enabled', BNPLS: - 'https://woo.com/document/woopayments/payment-methods/buy-now-pay-later/#contact-support', + 'https://woocommerce.com/document/woopayments/payment-methods/buy-now-pay-later/#contact-support', }; export const getDocumentationUrlForDisabledPaymentMethod = ( diff --git a/client/components/payment-methods-list/payment-method.tsx b/client/components/payment-methods-list/payment-method.tsx index 7213407e66c..598f29e48fe 100644 --- a/client/components/payment-methods-list/payment-method.tsx +++ b/client/components/payment-methods-list/payment-method.tsx @@ -200,7 +200,7 @@ const PaymentMethod = ( { 'woocommerce-payments' ) } href={ - 'https://woo.com/my-account/contact-support/' + 'https://woocommerce.com/my-account/contact-support/' } /> ), @@ -233,7 +233,7 @@ const PaymentMethod = ( { /* eslint-disable-next-line max-len */ href={ isPoInProgress - ? 'https://woo.com/document/woopayments/startup-guide/gradual-signup/#additional-payment-methods' + ? 'https://woocommerce.com/document/woopayments/startup-guide/gradual-signup/#additional-payment-methods' : getDocumentationUrlForDisabledPaymentMethod( paymentMethodId ) @@ -270,8 +270,6 @@ const PaymentMethod = ( { checked={ checked } disabled={ disabled || locked } onChange={ handleChange } - delayMsOnCheck={ 1500 } - delayMsOnUncheck={ 0 } hideLabel isAllowingManualCapture={ isAllowingManualCapture } isSetupRequired={ isSetupRequired } @@ -350,7 +348,7 @@ const PaymentMethod = ( { ) } diff --git a/client/components/payments-activity/index.tsx b/client/components/payments-activity/index.tsx new file mode 100644 index 00000000000..805e26b961d --- /dev/null +++ b/client/components/payments-activity/index.tsx @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import * as React from 'react'; +import { Card, CardBody, CardHeader } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ + +import EmptyStateAsset from 'assets/images/payment-activity-empty-state.svg?asset'; +import interpolateComponents from '@automattic/interpolate-components'; +import PaymentsActivityData from './payments-activity-data'; +import './style.scss'; + +const PaymentsActivity: React.FC = () => { + const { lifetimeTPV } = wcpaySettings; + const hasAtLeastOnePayment = lifetimeTPV > 0; + + return ( + + + { __( 'Your payment activity', 'woocommerce-payments' ) } + + { hasAtLeastOnePayment && <>{ /* Filters go here */ } } + + + { hasAtLeastOnePayment ? ( + + ) : ( +
+ +

+ { interpolateComponents( { + mixedString: __( + '{{strong}}No payments…yet!{{/strong}}' + ), + components: { + strong: , + }, + } ) } +

+

+ { __( + "Once your first order comes in, you'll start seeing your payment activity right here.", + 'woocommerce-payments' + ) } +

+
+ ) } +
+
+ ); +}; + +export default PaymentsActivity; diff --git a/client/components/payments-activity/payments-activity-data.tsx b/client/components/payments-activity/payments-activity-data.tsx new file mode 100644 index 00000000000..ab1ead1e3ea --- /dev/null +++ b/client/components/payments-activity/payments-activity-data.tsx @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import * as React from 'react'; +import { __ } from '@wordpress/i18n'; +import HelpOutlineIcon from 'gridicons/dist/help-outline'; + +/** + * Internal dependencies. + */ +import PaymentsDataTile from './payments-data-tile'; +import { ClickTooltip } from '../tooltip'; +import { getAdminUrl } from 'wcpay/utils'; +import './style.scss'; + +const PaymentsActivityData: React.FC = () => { + return ( +
+ } + buttonLabel={ __( + 'Total payments volume tooltip', + 'woocommerce-payments' + ) } + content={ __( + 'test total payments volume content', + 'woocommerce-payments' + ) } + /> + } + reportLink={ getAdminUrl( { + page: 'wc-admin', + path: '/payments/transactions', + } ) } + /> +
+ } + buttonLabel={ __( + 'Charges tooltip', + 'woocommerce-payments' + ) } + content={ __( 'test charge content' ) } + /> + } + reportLink={ getAdminUrl( { + page: 'wc-admin', + path: '/payments/transactions', + filter: 'advanced', + type_is: 'charge', + } ) } + /> + + + } + buttonLabel={ __( + 'Fees tooltip', + 'woocommerce-payments' + ) } + content={ __( + 'test fees content', + 'woocommerce-payments' + ) } + /> + } + /> +
+
+ ); +}; + +export default PaymentsActivityData; diff --git a/client/components/payments-activity/payments-data-tile.tsx b/client/components/payments-activity/payments-data-tile.tsx new file mode 100644 index 00000000000..3d2648c79b8 --- /dev/null +++ b/client/components/payments-activity/payments-data-tile.tsx @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import * as React from 'react'; +import { __ } from '@wordpress/i18n'; +import { Link } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { formatCurrency } from 'wcpay/utils/currency'; +import Loadable from '../loadable'; + +interface PaymentsDataTileProps { + /** + * The id for the tile, can be used for CSS styling. + */ + id: string; + /** + * Label for the amount in the tile. + */ + label: string; + /** + * The currency code for the amount displayed. + */ + currencyCode: string; + /** + * For optionally passing a ClickTooltip component. + */ + tooltip?: React.ReactElement; + /** + * The amount to be displayed in the tile. + */ + amount?: number; + /** + * Loading state of the tile. + */ + isLoading?: boolean; + /** + * Optional hover link to view report. + */ + reportLink?: string; +} + +const PaymentsDataTile: React.FC< PaymentsDataTileProps > = ( { + id, + label, + currencyCode, + tooltip, + amount = 0, + isLoading = false, + reportLink, +} ) => { + return ( +
+

+ { label } + { ! isLoading && tooltip } +

+
+

+ +

+ { reportLink && ( + + { __( 'View report', 'woocommerce_payments' ) } + + ) } +
+
+ ); +}; + +export default PaymentsDataTile; diff --git a/client/components/payments-activity/style.scss b/client/components/payments-activity/style.scss new file mode 100644 index 00000000000..255cb620ca6 --- /dev/null +++ b/client/components/payments-activity/style.scss @@ -0,0 +1,151 @@ +@mixin label-styles { + font-size: 12px; + font-weight: 400; + line-height: 16px; + color: $gray-700; + margin-bottom: 8px; +} + +@mixin amount-styles { + font-size: 20px; + font-weight: 500; + color: $gray-900; + line-height: 28px; +} + +.wcpay-payments-activity { + &__card { + &__body { + padding: 0 !important; + &__empty-state-wrapper { + text-align: center; + color: #949494; + padding: 16px 0 19px; + } + } + } +} + +.wcpay-payments-activity-data { + display: grid; + grid-template-columns: 1fr 1fr; + width: 100%; + + @include breakpoint( '<660px' ) { + grid-template-columns: 1fr; + padding: 24px; + } + + .wcpay-payments-data-highlights { + display: grid; + grid-template-columns: 1fr 1fr; + + @include breakpoint( '<660px' ) { + grid-template-columns: 1fr; + } + + &__item { + padding: 24px; + border-left: 1px solid $gray-200; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + min-height: 129px; + + &:nth-of-type( 3 ), + &:nth-of-type( 4 ) { + @include breakpoint( '>660px' ) { + border-top: 1px solid $gray-200; + } + } + + &:nth-last-of-type( 1 ) { + @include breakpoint( '<660px' ) { + border-bottom: none; + padding-bottom: 0; + } + } + + &:hover { + .wcpay-payments-data-highlights__item__wrapper a { + opacity: 1; + } + } + + &__label { + @include label-styles; + margin: 0 0 8px 0; + } + + &__wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + + @include breakpoint( '<660px' ) { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + &__amount { + @include amount-styles; + margin: 0 0 8px 0; + + @include breakpoint( '<660px' ) { + margin: 0; + } + } + + a { + text-decoration: none; + font-size: 12px; + opacity: 0; + + &:focus { + opacity: 1; + } + + @include breakpoint( '<660px' ) { + min-height: unset; + opacity: 1; + } + } + } + + @include breakpoint( '<660px' ) { + border-left: none; + border-bottom: 1px solid $gray-200; + padding: 16px 0; + min-height: unset; + } + } + + @include breakpoint( '<660px' ) { + flex-direction: column; + } + } +} + +#wcpay-payments-activity-data { + &__total-payments-volume { + border-left: none; + align-self: stretch; + + &__label { + @include label-styles; + } + + &__amount { + @include amount-styles; + } + + @include breakpoint( '<660px' ) { + border-bottom: 1px solid $gray-200; + padding-top: 0; + padding-bottom: 24px; + } + } +} diff --git a/client/components/payments-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payments-activity/test/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..b20840c408e --- /dev/null +++ b/client/components/payments-activity/test/__snapshots__/index.test.tsx.snap @@ -0,0 +1,323 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentsActivity component should render 1`] = ` +
+
+
+
+ Your payment activity +
+
+
+ +
+
+

+ + Charges + + +

+
+

+ €3,143.00 +

+ + View report + +
+
+
+

+ + Refunds + +

+
+

+ €1,532.00 +

+ + View report + +
+
+
+

+ + Disputes + +

+
+

+ €47.27 +

+ + View report + +
+
+
+

+ + Fees + + +

+
+

+ €94.29 +

+
+
+
+
+
+
+ +`; + +exports[`PaymentsActivity component should render an empty state 1`] = ` +
+
+
+
+ Your payment activity +
+
+
+ +

+ + No payments…yet! + +

+

+ Once your first order comes in, you'll start seeing your payment activity right here. +

+
+
+
+ +`; diff --git a/client/components/payments-activity/test/__snapshots__/payments-data-tile.test.tsx.snap b/client/components/payments-activity/test/__snapshots__/payments-data-tile.test.tsx.snap new file mode 100644 index 00000000000..4559092d8ff --- /dev/null +++ b/client/components/payments-activity/test/__snapshots__/payments-data-tile.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentsDataTile renders correctly 1`] = ` +
+
+

+ + Total Payments + +

+
+

+ $0.00 +

+
+
+
+`; diff --git a/client/components/payments-activity/test/index.test.tsx b/client/components/payments-activity/test/index.test.tsx new file mode 100644 index 00000000000..2616e6be629 --- /dev/null +++ b/client/components/payments-activity/test/index.test.tsx @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import PaymentsActivity from '..'; + +declare const global: { + wcpaySettings: { + lifetimeTPV: number; + accountStatus: { + deposits: { + restrictions: string; + completed_waiting_period: boolean; + minimum_scheduled_deposit_amounts: { + [ currencyCode: string ]: number; + }; + }; + }; + accountDefaultCurrency: string; + zeroDecimalCurrencies: string[]; + currencyData: Record< string, any >; + connect: { + country: string; + }; + }; +}; + +describe( 'PaymentsActivity component', () => { + beforeEach( () => { + global.wcpaySettings = { + lifetimeTPV: 1000, + accountStatus: { + deposits: { + restrictions: 'deposits_unrestricted', + completed_waiting_period: true, + minimum_scheduled_deposit_amounts: { + eur: 500, + usd: 500, + }, + }, + }, + accountDefaultCurrency: 'USD', + zeroDecimalCurrencies: [], + connect: { + country: 'US', + }, + currencyData: { + US: { + code: 'USD', + symbol: '$', + symbolPosition: 'left', + thousandSeparator: ',', + decimalSeparator: '.', + precision: 2, + }, + EU: { + code: 'EUR', + symbol: '€', + symbolPosition: 'left', + thousandSeparator: '.', + decimalSeparator: ',', + precision: 2, + }, + }, + }; + } ); + + it( 'should render', () => { + const { container } = render( ); + + expect( container ).toMatchSnapshot(); + } ); + + it( 'should render an empty state', () => { + global.wcpaySettings.lifetimeTPV = 0; + + const { container, getByText } = render( ); + + expect( getByText( 'No payments…yet!' ) ).toBeInTheDocument(); + expect( container ).toMatchSnapshot(); + } ); +} ); diff --git a/client/components/payments-activity/test/payments-data-tile.test.tsx b/client/components/payments-activity/test/payments-data-tile.test.tsx new file mode 100644 index 00000000000..ada38673cd0 --- /dev/null +++ b/client/components/payments-activity/test/payments-data-tile.test.tsx @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import PaymentsDataTile from '../payments-data-tile'; + +declare const global: { + wcpaySettings: { + accountDefaultCurrency: string; + zeroDecimalCurrencies: string[]; + currencyData: Record< string, any >; + connect: { + country: string; + }; + }; +}; + +describe( 'PaymentsDataTile', () => { + global.wcpaySettings = { + accountDefaultCurrency: 'USD', + zeroDecimalCurrencies: [], + connect: { + country: 'US', + }, + currencyData: { + US: { + code: 'USD', + symbol: '$', + symbolPosition: 'left', + thousandSeparator: ',', + decimalSeparator: '.', + precision: 2, + }, + }, + }; + + test( 'renders correctly', () => { + const { container } = render( + + ); + expect( container ).toMatchSnapshot(); + } ); + + test( 'renders label correctly', () => { + const label = 'Total Payments'; + render( + + ); + const labelElement = screen.getByText( label ); + expect( labelElement ).toBeInTheDocument(); + } ); + + test( 'renders amount correctly', () => { + const amount = 10000; + const currencyCode = 'USD'; + render( + + ); + const amountElement = screen.getByText( '$100.00' ); + expect( amountElement ).toBeInTheDocument(); + } ); + + test( 'renders report link correctly', () => { + const reportLink = 'https://example.com/report'; + render( + + ); + const reportLinkElement = screen.getByRole( 'link', { + name: 'View report', + } ); + expect( reportLinkElement ).toBeInTheDocument(); + expect( reportLinkElement ).toHaveAttribute( 'href', reportLink ); + } ); +} ); diff --git a/client/components/test-mode-notice/index.tsx b/client/components/test-mode-notice/index.tsx index f77dcaff4a7..42b1198f62c 100644 --- a/client/components/test-mode-notice/index.tsx +++ b/client/components/test-mode-notice/index.tsx @@ -11,6 +11,7 @@ import { getPaymentSettingsUrl, isInTestMode } from 'utils'; import BannerNotice from '../banner-notice'; import interpolateComponents from '@automattic/interpolate-components'; import { Link } from '@woocommerce/components'; +import { recordEvent } from 'wcpay/tracks'; type CurrentPage = | 'overview' @@ -87,10 +88,16 @@ const getNoticeContent = ( // eslint-disable-next-line jsx-a11y/anchor-has-content + recordEvent( + 'wcpay_overview_test_mode_learn_more_clicked' + ) + } /> ), }, diff --git a/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap b/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap index babe7208736..d3f06d3f8bb 100644 --- a/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap +++ b/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap @@ -107,8 +107,8 @@ exports[`Test mode notification Returns valid component for overview page 1`] =
All transactions will be simulated. diff --git a/client/components/tooltip/test/index.test.tsx b/client/components/tooltip/test/index.test.tsx index dee0a0fdf2e..cdbc146dd5f 100644 --- a/client/components/tooltip/test/index.test.tsx +++ b/client/components/tooltip/test/index.test.tsx @@ -242,7 +242,7 @@ describe( 'ClickTooltip', () => { content={ // Tooltip content includes a link element which should be navigable via keyboard - Tooltip content Link + Tooltip content Link } onHide={ handleHideMock } diff --git a/client/components/woopay/save-user/checkout-page-save-user.js b/client/components/woopay/save-user/checkout-page-save-user.js index 170715eb9ac..10baef0f587 100644 --- a/client/components/woopay/save-user/checkout-page-save-user.js +++ b/client/components/woopay/save-user/checkout-page-save-user.js @@ -270,7 +270,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { learnMore: ( { recordUserEvent( diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 1696db52b2d..7cb4fdf9a15 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -31,6 +31,12 @@ import strings from './strings'; import './style.scss'; import InlineNotice from 'components/inline-notice'; +const SandboxModeNotice = () => ( + + { strings.sandboxModeNotice } + +); + const ConnectAccountPage: React.FC = () => { const firstName = wcSettings.admin?.currentUserData?.first_name; const incentive = wcpaySettings.connectIncentive; @@ -50,12 +56,6 @@ const ConnectAccountPage: React.FC = () => { const isCountrySupported = !! availableCountries[ country ]; - const SandboxModeNotice = () => ( - - { strings.sandboxModeNotice } - - ); - useEffect( () => { recordEvent( 'page_view', { path: 'payments_connect_v2', diff --git a/client/connect-account-page/modal/index.js b/client/connect-account-page/modal/index.js index 617ce85731f..c3cb728c043 100644 --- a/client/connect-account-page/modal/index.js +++ b/client/connect-account-page/modal/index.js @@ -15,7 +15,7 @@ import './style.scss'; const LearnMoreLink = ( props ) => ( @@ -216,7 +216,7 @@ exports[`Onboarding: location check dialog renders correctly when opened 1`] = ` diff --git a/client/connect-account-page/payment-methods.tsx b/client/connect-account-page/payment-methods.tsx index 5d99de86599..f82adcff7a3 100644 --- a/client/connect-account-page/payment-methods.tsx +++ b/client/connect-account-page/payment-methods.tsx @@ -15,13 +15,13 @@ import { AmericanExpressIcon, ApplePayIcon, CBIcon, - DinersClubIcon, + IdealIcon, DiscoverIcon, GooglePayIcon, MastercardIcon, - SofortIcon, VisaIcon, WooIcon, + KlarnaIcon, } from 'wcpay/payment-methods-icons'; const PaymentMethods: React.FC = () => { @@ -33,19 +33,18 @@ const PaymentMethods: React.FC = () => { - + - { wcpaySettings.isWooPayStoreCountryAvailable && } - + { 'GB' === wcpaySettings?.connect?.country ? ( ) : ( ) } - & more. + & more
); diff --git a/client/connect-account-page/strings.tsx b/client/connect-account-page/strings.tsx index 2af5229ac6f..862a2398acd 100644 --- a/client/connect-account-page/strings.tsx +++ b/client/connect-account-page/strings.tsx @@ -38,62 +38,6 @@ export default { 'Earn recurring revenue and get deposits into your bank account.', 'woocommerce-payments' ), - agreement: createInterpolateElement( - __( - 'By clicking “Finish setup”, you agree to the Terms of Service and acknowledge that you have read our Privacy Policy.', - 'woocommerce-payments' - ), - { - a1: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - a2: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - } - ), - agreementWithWooPay: createInterpolateElement( - __( - 'By clicking “Finish setup”, you agree to the Terms of Service (including WooPay merchant terms) and acknowledge that you have read our Privacy Policy.', - 'woocommerce-payments' - ), - { - a1: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - a2: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - a3: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - } - ), sandboxMode: { title: __( "I'm setting up a store for someone else.", @@ -118,7 +62,7 @@ export default { // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. // eslint-disable-next-line jsx-a11y/anchor-has-content @@ -338,7 +282,7 @@ export default { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content diff --git a/client/connect-account-page/test/__snapshots__/index.test.tsx.snap b/client/connect-account-page/test/__snapshots__/index.test.tsx.snap index 8cf485187f6..4a76f5396d4 100644 --- a/client/connect-account-page/test/__snapshots__/index.test.tsx.snap +++ b/client/connect-account-page/test/__snapshots__/index.test.tsx.snap @@ -87,9 +87,9 @@ exports[`ConnectAccountPage should render correctly 1`] = ` src="assets/images/cards/discover.svg" /> Diners Club Apple Pay Sofort Affirm - & more. + & more
Diners Club Apple Pay WooPay - Sofort Affirm - & more. + & more
Diners Club Apple Pay Sofort Affirm - & more. + & more
{ } ); test( 'before saving sets isSaving to true, and after - to false', () => { - apiFetch.mockReturnValue( 'api request' ); - - const yielded = [ ...saveSettings() ]; + const apiResponse = { + data: { + payment_method_statuses: { + bancontact: 'active', + }, + }, + }; + apiFetch.mockReturnValue( { ...apiResponse } ); - const apiRequestIndex = yielded.indexOf( 'api request' ); + const saveGenerator = saveSettings(); - const isSavingStartIndex = findIndex( - yielded, + // Assert the first yield is updating isSaving to true + let next = saveGenerator.next(); + expect( next.value ).toEqual( updateIsSavingSettings( true, null ) ); - const isSavingEndIndex = findIndex( - yielded, + // Execute the next step, which should be the apiFetch call + next = saveGenerator.next(); + expect( next.value ).toEqual( apiResponse ); + + // Simulate the response from the apiFetch call and proceed to the next yield + // Since the actual fetching process is mocked, pass the apiResponse to the next saveGenerator step directly + next = saveGenerator.next( apiResponse ); + expect( next.value ).toEqual( { + type: 'SET_SETTINGS_VALUES', + payload: { + payment_method_statuses: + apiResponse.data.payment_method_statuses, + }, + } ); + + next = saveGenerator.next(); // Skip the success notice + next = saveGenerator.next(); // Move to updateIsSavingSettings(false) + expect( next.value ).toEqual( updateIsSavingSettings( false, null ) ); - expect( apiRequestIndex ).not.toEqual( -1 ); - expect( isSavingStartIndex ).toBeLessThan( apiRequestIndex ); - expect( isSavingEndIndex ).toBeGreaterThan( apiRequestIndex ); + // Check if the saveGenerator is complete + expect( saveGenerator.next().done ).toBeTruthy(); } ); test( 'displays success notice after saving', () => { - // eslint-disable-next-line no-unused-expressions - [ ...saveSettings() ]; + const apiResponse = { + data: { + payment_method_statuses: { + bancontact: 'active', + }, + }, + }; + apiFetch.mockReturnValue( { ...apiResponse } ); + + // Execute the generator until the end + const saveGenerator = saveSettings(); + while ( ! saveGenerator.next( apiResponse ).done ) { + // Intentionally empty + } + expect( saveGenerator.next().done ).toBeTruthy(); expect( dispatch( 'core/notices' ).createSuccessNotice diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 87e46859cfe..4d47ae51d6a 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -231,7 +231,7 @@ export const DepositDetails: React.FC< DepositDetailsProps > = ( { ), components: { learnMoreLink: ( - + ), }, } ) } diff --git a/client/deposits/index.tsx b/client/deposits/index.tsx index c71a368d8d7..df0d6462a51 100644 --- a/client/deposits/index.tsx +++ b/client/deposits/index.tsx @@ -20,6 +20,7 @@ import { useAllDepositsOverviews } from 'data'; import { useSettings } from 'wcpay/data'; import DepositsList from './list'; import { hasAutomaticScheduledDeposits } from 'wcpay/deposits/utils'; +import { recordEvent } from 'wcpay/tracks'; const useNextDepositNoticeState = () => { const { updateOptions } = useDispatch( 'wc/admin/options' ); @@ -99,6 +100,7 @@ const NextDepositNotice: React.FC = () => { const DepositFailureNotice: React.FC = () => { const { hasErroredExternalAccount } = useAccountStatus(); + const accountLink = wcpaySettings.accountStatus.accountLink; return hasErroredExternalAccount ? ( { components: { updateLink: ( + recordEvent( + 'wcpay_account_details_link_clicked', + { source: 'deposits' } + ) + } + href={ accountLink } /> ), }, diff --git a/client/deposits/instant-deposits/modal.tsx b/client/deposits/instant-deposits/modal.tsx index fffad75ff88..25b2783a348 100644 --- a/client/deposits/instant-deposits/modal.tsx +++ b/client/deposits/instant-deposits/modal.tsx @@ -29,7 +29,7 @@ const InstantDepositModal: React.FC< InstantDepositModalProps > = ( { inProgress, } ) => { const learnMoreHref = - 'https://woo.com/document/woopayments/deposits/instant-deposits/'; + 'https://woocommerce.com/document/woopayments/deposits/instant-deposits/'; const feePercentage = `${ percentage }%`; const description = createInterpolateElement( /* translators: %s: amount representing the fee percentage, : instant payout doc URL */ diff --git a/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap b/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap index 3100b40bed7..6d9772123f8 100644 --- a/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap +++ b/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap @@ -69,7 +69,7 @@ exports[`Instant deposit button and modal modal renders correctly 1`] = `

Need cash in a hurry? Instant deposits are available within 30 minutes for a nominal 1.5% service fee. diff --git a/client/disable-confirmation-modal/index.js b/client/disable-confirmation-modal/index.js index 209fd1e842b..d8f96db2e28 100644 --- a/client/disable-confirmation-modal/index.js +++ b/client/disable-confirmation-modal/index.js @@ -175,11 +175,11 @@ const DisableConfirmationModal = ( { onClose, onConfirm } ) => { strong: , wooCommercePaymentsLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content - + ), contactSupportLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content - + ), }, } ) } diff --git a/client/globals.d.ts b/client/globals.d.ts index cb3cb7d9a8d..9f2e38eb39f 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -97,6 +97,7 @@ declare global { onboardingFieldsData?: { business_types: Country[]; mccs_display_tree: MccsDisplayTreeItem[]; + industry_to_mcc: { [ key: string ]: string }; }; storeCurrency: string; isMultiCurrencyEnabled: string; @@ -126,6 +127,7 @@ declare global { trackingInfo?: { hosting_provider: string; }; + lifetimeTPV: number; }; const wc: { @@ -150,6 +152,7 @@ declare global { onboarding: { profile: { wccom_connected: boolean; + industry?: string[]; }; }; currentUserData: { diff --git a/client/index.js b/client/index.js index 3615fe1d07b..1e8c996b47a 100644 --- a/client/index.js +++ b/client/index.js @@ -300,6 +300,7 @@ addFilter( const wcPayTasks = getTasks( { showUpdateDetailsTask: showUpdateDetailsTask, wpcomReconnectUrl: wpcomReconnectUrl, + showGoLiveTask: true, } ); return [ ...tasks, ...wcPayTasks ]; diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js b/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js index 6f1f07f4beb..8d08b90ef11 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js @@ -25,7 +25,7 @@ import SettingsSection from 'wcpay/settings/settings-section'; const EnabledCurrenciesSettingsDescription = () => { const LEARN_MORE_URL = - 'https://woo.com/document/woopayments/currencies/multi-currency-setup/#enabled-currencies'; + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#enabled-currencies'; return ( <> diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap b/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap index 17058bf57ca..982e77f82a9 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap @@ -14,7 +14,7 @@ exports[`Multi-Currency enabled currencies list Enabled currencies list renders

Accept payments in multiple currencies. Prices are converted based on exchange rates and rounding rules. diff --git a/client/multi-currency/multi-currency-settings/store-settings/index.js b/client/multi-currency/multi-currency-settings/store-settings/index.js index 62c04258ed0..eddeb570ccf 100644 --- a/client/multi-currency/multi-currency-settings/store-settings/index.js +++ b/client/multi-currency/multi-currency-settings/store-settings/index.js @@ -18,7 +18,7 @@ import PreviewModal from 'wcpay/multi-currency/preview-modal'; const StoreSettingsDescription = () => { const LEARN_MORE_URL = - 'https://woo.com/document/woopayments/currencies/multi-currency-setup/#store-settings'; + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#store-settings'; return ( <> diff --git a/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap b/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap index 9e1803ae8ab..b0ddd6f2af1 100644 --- a/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap +++ b/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap @@ -14,7 +14,7 @@ exports[`Multi-Currency store settings store settings task renders correctly: sn

Store settings allow your customers to choose which currency they would like to use when shopping at your store. diff --git a/client/multi-currency/single-currency-settings/index.js b/client/multi-currency/single-currency-settings/index.js index 7bdb5a33f6b..8285f206dd8 100644 --- a/client/multi-currency/single-currency-settings/index.js +++ b/client/multi-currency/single-currency-settings/index.js @@ -417,7 +417,7 @@ const SingleCurrencySettings = () => { onClick={ () => { open( /* eslint-disable-next-line max-len */ - 'https://woo.com/document/woopayments/currencies/multi-currency-setup/#price-rounding', + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#price-rounding', '_blank' ); } } @@ -482,7 +482,7 @@ const SingleCurrencySettings = () => { onClick={ () => { open( /* eslint-disable-next-line max-len */ - 'https://woo.com/document/woopayments/currencies/multi-currency-setup/#charm-pricing', + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#charm-pricing', '_blank' ); } } diff --git a/client/onboarding/index.tsx b/client/onboarding/index.tsx index 42d88da0dbf..4100d0426d9 100644 --- a/client/onboarding/index.tsx +++ b/client/onboarding/index.tsx @@ -9,22 +9,22 @@ import React, { useEffect } from 'react'; import Page from 'components/page'; import { OnboardingContextProvider } from './context'; import { Stepper } from 'components/stepper'; +import { getMccFromIndustry } from 'onboarding/utils'; import { OnboardingForm } from './form'; import Step from './step'; import BusinessDetails from './steps/business-details'; import StoreDetails from './steps/store-details'; import LoadingStep from './steps/loading'; import { trackStarted } from './tracking'; +import { getAdminUrl } from 'wcpay/utils'; import './style.scss'; const OnboardingStepper = () => { const handleExit = () => { - if ( - window.history.length > 1 && - document.referrer.includes( wcSettings.adminUrl ) - ) - return window.history.back(); - window.location.href = wcSettings.adminUrl; + window.location.href = getAdminUrl( { + page: 'wc-admin', + path: '/payments/connect', + } ); }; const handleStepChange = () => window.scroll( 0, 0 ); @@ -48,6 +48,7 @@ const OnboardingStepper = () => { const initialData = { business_name: wcSettings?.siteTitle, + mcc: getMccFromIndustry(), url: location.hostname === 'localhost' ? 'https://wcpay.test' diff --git a/client/onboarding/steps/business-details.tsx b/client/onboarding/steps/business-details.tsx index 42a7ad5bbb2..79ad8d7ba09 100644 --- a/client/onboarding/steps/business-details.tsx +++ b/client/onboarding/steps/business-details.tsx @@ -25,9 +25,15 @@ const BusinessDetails: React.FC = () => { const businessTypes = getBusinessTypes(); const mccsFlatList = getMccsFlatList(); - const selectedCountry = businessTypes.find( - ( country ) => country.key === data.country - ); + const selectedCountry = businessTypes.find( ( country ) => { + // Special case for Puerto Rico as it's considered a separate country in Core, but the business country should be US + if ( data.country === 'PR' ) { + return country.key === 'US'; + } + + return country.key === data.country; + } ); + const selectedBusinessType = selectedCountry?.types.find( ( type ) => type.key === data.business_type ); diff --git a/client/onboarding/steps/test/business-details.tsx b/client/onboarding/steps/test/business-details.tsx index 2bfe392b532..081467c8370 100644 --- a/client/onboarding/steps/test/business-details.tsx +++ b/client/onboarding/steps/test/business-details.tsx @@ -11,7 +11,6 @@ import { mocked } from 'ts-jest/utils'; */ import BusinessDetails from '../business-details'; import { OnboardingContextProvider } from '../../context'; -import strings from '../../strings'; import { getAvailableCountries, getBusinessTypes, diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 9408cd1e1ef..7a5efb46b0c 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -186,4 +186,9 @@ body.wcpay-onboarding__body { color: $gray-700; } } + + // Hide Jetpack's JITM (Just in time messages) banners for onboarding. + .woocommerce-layout__jitm { + display: none; + } } diff --git a/client/onboarding/utils.ts b/client/onboarding/utils.ts index c45c218f9b0..a427f3c3a93 100644 --- a/client/onboarding/utils.ts +++ b/client/onboarding/utils.ts @@ -42,6 +42,23 @@ export const getBusinessTypes = (): Country[] => { ); }; +/** + * Get the MCC code for the selected industry. + * + * @return {string | undefined} The MCC code for the selected industry. Will return undefined if no industry is selected. + */ +export const getMccFromIndustry = (): string | undefined => { + const industry = wcSettings.admin.onboarding.profile.industry?.[ 0 ]; + if ( ! industry ) { + return undefined; + } + + const industryToMcc = + wcpaySettings?.onboardingFieldsData?.industry_to_mcc || {}; + + return industryToMcc[ industry ]; +}; + export const getMccsFlatList = (): ListItem[] => { const data = wcpaySettings?.onboardingFieldsData?.mccs_display_tree; diff --git a/client/order/order-status-change-strategies/index.tsx b/client/order/order-status-change-strategies/index.tsx index 2216a3c3ba2..9a4fc135834 100644 --- a/client/order/order-status-change-strategies/index.tsx +++ b/client/order/order-status-change-strategies/index.tsx @@ -71,7 +71,7 @@ function triggerCancelAuthorizationModal( @@ -84,7 +84,7 @@ function triggerCancelAuthorizationModal( cancelAuthorization: ( { __( 'cancel the payment', 'woocommerce-payments' ) } @@ -140,7 +140,7 @@ function triggerCaptureAuthorizationModal( @@ -154,7 +154,7 @@ function triggerCaptureAuthorizationModal( @@ -271,7 +271,7 @@ function handleCancelledStatus( howtoIssueRefunds: ( { __( 'how to issue refunds', 'woocommerce-payments' ) } diff --git a/client/order/test-mode-notice/index.tsx b/client/order/test-mode-notice/index.tsx index 7778f38a65a..5e30d8ffda2 100644 --- a/client/order/test-mode-notice/index.tsx +++ b/client/order/test-mode-notice/index.tsx @@ -22,7 +22,7 @@ const TestModeNotice = (): JSX.Element => { learnMoreLink: ( { __( diff --git a/client/overview/connection-sucess-notice.tsx b/client/overview/connection-sucess-notice.tsx index 5a0e4c0af16..634d84c14fd 100644 --- a/client/overview/connection-sucess-notice.tsx +++ b/client/overview/connection-sucess-notice.tsx @@ -40,46 +40,25 @@ const ConnectionSuccessNotice: React.FC = () => { /> ); }; - - return ! isDismissed && ! onboardingTestMode ? ( + const isPoDisabledOrCompleted = ! isPoEnabled || isPoComplete; + return ! isDismissed && ! onboardingTestMode && isPoDisabledOrCompleted ? ( - { /* Show dismiss button only at the end of Progressive Onboarding // - or at the end of the full KYC flow. */ } - { ! ( isPoEnabled && ! isPoComplete ) && } + confetti - { isPoEnabled && ! isPoComplete ? ( - <> -

- { __( - "You're ready to start selling!", - 'woocommerce-payments' - ) } -

-

- { __( - 'Congratulations! Take a moment to celebrate and look out for the first sale.', - 'woocommerce-payments' - ) } -

- + { accountStatus !== 'complete' ? ( +

+ { __( + 'Congratulations! Your store is being verified.', + 'woocommerce-payments' + ) } +

) : ( - <> - { accountStatus !== 'complete' ? ( -

- { __( - 'Congratulations! Your store is being verified.', - 'woocommerce-payments' - ) } -

- ) : ( -

- { __( - 'Congratulations! Your store has been verified.', - 'woocommerce-payments' - ) } -

+

+ { __( + 'Congratulations! Your store has been verified.', + 'woocommerce-payments' ) } - +

) } ) : null; diff --git a/client/overview/index.js b/client/overview/index.js index 8f256db0980..c83d4a062aa 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -4,32 +4,38 @@ * External dependencies */ import React, { useState } from 'react'; -import { Card, Notice } from '@wordpress/components'; +import { Button, Card, Notice } from '@wordpress/components'; import { getQuery } from '@woocommerce/navigation'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies. */ -import Page from 'components/page'; -import { TestModeNotice } from 'components/test-mode-notice'; -import AccountStatus from 'components/account-status'; -import Welcome from 'components/welcome'; import AccountBalances from 'components/account-balances'; -import DepositsOverview from 'components/deposits-overview'; +import AccountStatus from 'components/account-status'; import ActiveLoanSummary from 'components/active-loan-summary'; +import ConnectionSuccessNotice from './connection-sucess-notice'; +import DepositsOverview from 'components/deposits-overview'; import ErrorBoundary from 'components/error-boundary'; -import TaskList from './task-list'; -import { getTasks, taskSort } from './task-list/tasks'; +import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; +import JetpackIdcNotice from 'components/jetpack-idc-notice'; +import Page from 'components/page'; +import PaymentsActivity from 'wcpay/components/payments-activity'; +import Welcome from 'components/welcome'; +import { TestModeNotice } from 'components/test-mode-notice'; import InboxNotifications from './inbox-notifications'; -import ConnectionSuccessNotice from './connection-sucess-notice'; import ProgressiveOnboardingEligibilityModal from './modal/progressive-onboarding-eligibility'; -import JetpackIdcNotice from 'components/jetpack-idc-notice'; -import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; -import { useDisputes, useGetSettings, useSettings } from 'wcpay/data'; -import strings from './strings'; -import './style.scss'; import SetupLivePaymentsModal from './modal/setup-live-payments'; +import TaskList from './task-list'; +import { getTasks, taskSort } from './task-list/tasks'; +import { useDisputes, useGetSettings, useSettings } from 'data'; +import './style.scss'; +import BannerNotice from 'wcpay/components/banner-notice'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; +import { recordEvent } from 'wcpay/tracks'; +import { ClickTooltip } from 'wcpay/components/tooltip'; +import HelpOutlineIcon from 'gridicons/dist/help-outline'; const OverviewPageError = () => { const queryParams = getQuery(); @@ -52,13 +58,85 @@ const OverviewPageError = () => { ); }; +const OverviewSandboxModeNotice = ( { ctaAction = () => {} } ) => { + return ( + + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + // eslint-disable-next-line max-len + '{{strong}}%1$s is in sandbox mode.{{/strong}} To accept real transactions, {{switchToLiveLink}}set up a live %1$s account{{/switchToLiveLink}}.{{learnMoreIcon/}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + strong: , + learnMoreIcon: ( + } + buttonLabel={ __( + 'Learn more about sandbox mode', + 'woocommerce-payments' + ) } + maxWidth={ '315px' } + content={ + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + // eslint-disable-next-line max-len + 'In sandbox mode, personal/business verifications and checkout payments are simulated. Find out what works best for you by {{strong}}testing all the %1$s options and flows.{{/strong}} {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + strong: , + learnMoreLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + recordEvent( + 'wcpay_overview_sandbox_mode_learn_more_clicked' + ) + } + /> + ), + }, + } ) } + + } + /> + ), + switchToLiveLink: ( +
-
-

- -
diff --git a/client/overview/task-list/strings.tsx b/client/overview/task-list/strings.tsx index 8ca397211ed..2be3305f6a3 100644 --- a/client/overview/task-list/strings.tsx +++ b/client/overview/task-list/strings.tsx @@ -422,5 +422,12 @@ export default { 'woocommerce-payments' ), }, + go_live: { + title: __( + 'Set up real payments on your store', + 'woocommerce-payments' + ), + time: __( '10 minutes', 'woocommerce-payments' ), + }, }, }; diff --git a/client/overview/task-list/tasks.tsx b/client/overview/task-list/tasks.tsx index eb1fbf3cfc6..4c5830eec24 100644 --- a/client/overview/task-list/tasks.tsx +++ b/client/overview/task-list/tasks.tsx @@ -18,6 +18,8 @@ import { getUpdateBusinessDetailsTask } from './tasks/update-business-details-ta import { CachedDispute } from 'wcpay/types/disputes'; import { TaskItemProps } from './types'; import { getAddApmsTask } from './tasks/add-apms-task'; +import { getGoLiveTask } from './tasks/go-live-task'; +import { isInDevMode } from 'wcpay/utils'; // Requirements we don't want to show to the user because they are too generic/not useful. These refer to Stripe error codes. const requirementBlacklist = [ 'invalid_value_other' ]; @@ -27,6 +29,7 @@ interface TaskListProps { wpcomReconnectUrl: string; activeDisputes?: CachedDispute[]; enabledPaymentMethods?: string[]; + showGoLiveTask: boolean; } export const getTasks = ( { @@ -34,6 +37,7 @@ export const getTasks = ( { wpcomReconnectUrl, activeDisputes = [], enabledPaymentMethods = [], + showGoLiveTask = false, }: TaskListProps ): TaskItemProps[] => { const { status, @@ -84,6 +88,8 @@ export const getTasks = ( { detailsSubmitted && ! isPoInProgress; + const isGoLiveTaskVisible = isInDevMode( false ) && showGoLiveTask; + return [ isUpdateDetailsTaskVisible && getUpdateBusinessDetailsTask( @@ -98,6 +104,7 @@ export const getTasks = ( { isDisputeTaskVisible && getDisputeResolutionTask( activeDisputes ), isPoEnabled && detailsSubmitted && getVerifyBankAccountTask(), isAddApmsTaskVisible && getAddApmsTask(), + isGoLiveTaskVisible && getGoLiveTask(), ].filter( Boolean ); }; diff --git a/client/overview/task-list/tasks/go-live-task.tsx b/client/overview/task-list/tasks/go-live-task.tsx new file mode 100644 index 00000000000..b0b5f41f439 --- /dev/null +++ b/client/overview/task-list/tasks/go-live-task.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import React, { useState } from 'react'; +import ReactDOM from 'react-dom'; + +/** + * Internal dependencies + */ +import type { TaskItemProps } from '../types'; +import strings from '../strings'; +import SetupLivePaymentsModal from '../../modal/setup-live-payments'; + +const SetupLivePaymentsModalWrapper: React.FC = () => { + const [ modalVisible, setModalVisible ] = useState( true ); + + return modalVisible ? ( + setModalVisible( false ) } /> + ) : ( + <> + ); +}; + +export const getGoLiveTask = (): TaskItemProps | null => { + const handleClick = () => { + const container = document.createElement( 'div' ); + container.id = 'wcpay-golivemodal-container'; + document.body.appendChild( container ); + ReactDOM.render( , container ); + }; + + return { + key: 'go-live-payments', + level: 3, + content: '', + title: strings.tasks.go_live.title, + time: strings.tasks.go_live.time, + completed: false, + onClick: handleClick, + action: handleClick, + expandable: false, + showActionButton: false, + }; +}; diff --git a/client/overview/task-list/tasks/po-task.tsx b/client/overview/task-list/tasks/po-task.tsx index 79aa738415e..f90b695b26e 100644 --- a/client/overview/task-list/tasks/po-task.tsx +++ b/client/overview/task-list/tasks/po-task.tsx @@ -10,6 +10,7 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies. */ import strings from '../strings'; +import { recordEvent } from 'wcpay/tracks'; const tpvLimit = 5000; @@ -27,6 +28,10 @@ export const getVerifyBankAccountTask = (): any => { } = wcpaySettings.accountStatus; const handleClick = () => { + recordEvent( 'wcpay_account_details_link_clicked', { + source: 'overview-page__receive-deposits-task', + } ); + window.location.href = addQueryArgs( wcpaySettings.connectUrl, { collect_payout_requirements: true, } ); diff --git a/client/overview/task-list/tasks/update-business-details-task.tsx b/client/overview/task-list/tasks/update-business-details-task.tsx index adb8e5b430f..9306b618f08 100644 --- a/client/overview/task-list/tasks/update-business-details-task.tsx +++ b/client/overview/task-list/tasks/update-business-details-task.tsx @@ -12,6 +12,7 @@ import type { TaskItemProps } from '../types'; import UpdateBusinessDetailsModal from 'wcpay/overview/modal/update-business-details'; import { dateI18n } from '@wordpress/date'; import moment from 'moment'; +import { recordEvent } from 'wcpay/tracks'; export const getUpdateBusinessDetailsTask = ( errorMessages: string[], @@ -105,6 +106,9 @@ export const getUpdateBusinessDetailsTask = ( if ( hasMultipleErrors ) { renderModal(); } else { + recordEvent( 'wcpay_account_details_link_clicked', { + source: 'update-business-details', + } ); window.open( accountLink, '_blank' ); } }; diff --git a/client/payment-details/dispute-details/dispute-notice.tsx b/client/payment-details/dispute-details/dispute-notice.tsx index e5cc583890d..9d9edd8a9f7 100644 --- a/client/payment-details/dispute-details/dispute-notice.tsx +++ b/client/payment-details/dispute-details/dispute-notice.tsx @@ -41,7 +41,7 @@ const DisputeNotice: React.FC< DisputeNoticeProps > = ( { 'woocommerce-payments' ); let learnMoreDocsUrl = - 'https://woo.com/document/woopayments/fraud-and-disputes/managing-disputes/#responding'; + 'https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/#responding'; if ( isInquiry( dispute.status ) ) { /* translators:
link to dispute inquiry documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */ @@ -51,7 +51,7 @@ const DisputeNotice: React.FC< DisputeNoticeProps > = ( { 'woocommerce-payments' ); learnMoreDocsUrl = - 'https://woo.com/document/woopayments/fraud-and-disputes/managing-disputes/#inquiries'; + 'https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/#inquiries'; } return ( diff --git a/client/payment-details/dispute-details/dispute-resolution-footer.tsx b/client/payment-details/dispute-details/dispute-resolution-footer.tsx index 545eee5d598..15fec759244 100644 --- a/client/payment-details/dispute-details/dispute-resolution-footer.tsx +++ b/client/payment-details/dispute-details/dispute-resolution-footer.tsx @@ -51,7 +51,7 @@ const DisputeUnderReviewFooter: React.FC< { ), } @@ -122,7 +122,7 @@ const DisputeWonFooter: React.FC< { ), } @@ -229,7 +229,7 @@ const DisputeLostFooter: React.FC< { ), } @@ -303,7 +303,7 @@ const InquiryUnderReviewFooter: React.FC< { ), } @@ -375,7 +375,7 @@ const InquiryClosedFooter: React.FC< { ), } diff --git a/client/payment-details/dispute-details/dispute-steps.tsx b/client/payment-details/dispute-details/dispute-steps.tsx index 923fab05edc..ccc0764f38b 100644 --- a/client/payment-details/dispute-details/dispute-steps.tsx +++ b/client/payment-details/dispute-details/dispute-steps.tsx @@ -107,7 +107,7 @@ export const DisputeSteps: React.FC< Props > = ( { ), { a: ( - + ), } ) } @@ -259,7 +259,7 @@ export const InquirySteps: React.FC< Props > = ( { ), { learnMoreLink: ( - + ), } ) } diff --git a/client/payment-details/dispute-details/dispute-summary-row.tsx b/client/payment-details/dispute-details/dispute-summary-row.tsx index 4f8b53fde94..ac6dada265e 100644 --- a/client/payment-details/dispute-details/dispute-summary-row.tsx +++ b/client/payment-details/dispute-details/dispute-summary-row.tsx @@ -65,7 +65,7 @@ const DisputeSummaryRow: React.FC< Props > = ( { dispute } ) => {

diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index a579feb6563..4350d609cd7 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -695,7 +695,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-no-target-blank diff --git a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap index cc2f878f0a1..8286a7941bb 100644 --- a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap @@ -291,7 +291,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca > You must @@ -643,7 +643,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th > You must @@ -3827,7 +3827,7 @@ exports[`PaymentDetailsSummary renders the information of a dispute-reversal cha > Good news! You won this dispute on -. The disputed amount and the dispute fee have been credited back to your account. diff --git a/client/payment-methods/capability-request/capability-request-map.ts b/client/payment-methods/capability-request/capability-request-map.ts index 69a3c556e9a..3c2cceebce6 100644 --- a/client/payment-methods/capability-request/capability-request-map.ts +++ b/client/payment-methods/capability-request/capability-request-map.ts @@ -27,7 +27,7 @@ const CapabilityRequestList: Array< CapabilityRequestMap > = [ ), actions: 'link', actionUrl: - 'https://woo.com/document/woopayments/payment-methods/#jcb', + 'https://woocommerce.com/document/woopayments/payment-methods/#jcb', actionsLabel: __( 'Finish setup', 'woocommerce-payments' ), }, pending: { diff --git a/client/payment-methods/constants.ts b/client/payment-methods/constants.ts index ac661f2a079..4ca2b2d7dc4 100644 --- a/client/payment-methods/constants.ts +++ b/client/payment-methods/constants.ts @@ -38,6 +38,7 @@ export const PAYMENT_METHOD_TITLES = { 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' ), diff --git a/client/payment-methods/delete-modal.tsx b/client/payment-methods/delete-modal.tsx index 381694faf79..16a39812a09 100644 --- a/client/payment-methods/delete-modal.tsx +++ b/client/payment-methods/delete-modal.tsx @@ -94,7 +94,7 @@ const ConfirmPaymentMethodDeleteModal: React.FunctionComponent< { ) } diff --git a/client/payment-request/blocks/payment-request-express.js b/client/payment-request/blocks/payment-request-express.js index 858f832394f..d0eca6e505a 100644 --- a/client/payment-request/blocks/payment-request-express.js +++ b/client/payment-request/blocks/payment-request-express.js @@ -75,8 +75,8 @@ const PaymentRequestExpressComponent = ( { onPaymentRequestAvailable( paymentRequestType ); } ); - const onPaymentRequestButtonClick = () => { - onButtonClick(); + const onPaymentRequestButtonClick = ( event ) => { + onButtonClick( event, paymentRequest ); const paymentRequestTypeEvents = { google_pay: 'gpay_button_click', @@ -84,8 +84,9 @@ const PaymentRequestExpressComponent = ( { }; if ( paymentRequestTypeEvents.hasOwnProperty( paymentRequestType ) ) { - const event = paymentRequestTypeEvents[ paymentRequestType ]; - recordUserEvent( event, { + const paymentRequestEvent = + paymentRequestTypeEvents[ paymentRequestType ]; + recordUserEvent( paymentRequestEvent, { source: wcpayPaymentRequestParams?.button_context, } ); } diff --git a/client/product-details/bnpl-site-messaging/index.js b/client/product-details/bnpl-site-messaging/index.js index 58a719dcf3f..a93981f5ef1 100644 --- a/client/product-details/bnpl-site-messaging/index.js +++ b/client/product-details/bnpl-site-messaging/index.js @@ -16,15 +16,28 @@ import apiRequest from 'wcpay/checkout/utils/request'; * @param {Object} api The API object used to save the UPE configuration. * @return {Promise} The appearance object for the UPE. */ -async function initializeAppearance( api ) { - const appearance = getUPEConfig( 'upeBnplProductPageAppearance' ); +const elementsLocations = { + bnplProductPage: { + configKey: 'upeBnplProductPageAppearance', + appearanceKey: 'bnpl_product_page', + }, + bnplClassicCart: { + configKey: 'upeBnplClassicCartAppearance', + appearanceKey: 'bnpl_classic_cart', + }, +}; + +async function initializeAppearance( api, location ) { + const { configKey, appearanceKey } = elementsLocations[ location ]; + + const appearance = getUPEConfig( configKey ); if ( appearance ) { return Promise.resolve( appearance ); } return await api.saveUPEAppearance( - getAppearance( 'bnpl_product_page' ), - 'bnpl_product_page' + getAppearance( appearanceKey ), + appearanceKey ); } @@ -36,31 +49,52 @@ export const initializeBnplSiteMessaging = async () => { accountId, publishableKey, paymentMethods, + currencyCode, + isCart, + isCartBlock, + cartTotal, } = window.wcpayStripeSiteMessaging; - const api = new WCPayAPI( - { - publishableKey: publishableKey, - accountId: accountId, - locale: locale, - }, - apiRequest - ); - const options = { - amount: parseInt( productVariations.base_product.amount, 10 ) || 0, - currency: productVariations.base_product.currency || 'USD', - paymentMethodTypes: paymentMethods || [], - countryCode: country, // Customer's country or base country of the store. - }; - const elementsOptions = { - appearance: await initializeAppearance( api ), - fonts: getFontRulesFromPage(), - }; - const paymentMessageElement = api - .getStripe() - .elements( elementsOptions ) - .create( 'paymentMethodMessaging', options ); - paymentMessageElement.mount( '#payment-method-message' ); + let amount; + let elementLocation = 'bnplProductPage'; + + if ( isCart || isCartBlock ) { + amount = parseInt( cartTotal, 10 ) || 0; + elementLocation = 'bnplClassicCart'; + } else { + amount = parseInt( productVariations.base_product.amount, 10 ) || 0; + } + + let paymentMessageElement; + + if ( ! isCartBlock ) { + const api = new WCPayAPI( + { + publishableKey: publishableKey, + accountId: accountId, + locale: locale, + }, + apiRequest + ); + + const options = { + amount: amount, + currency: currencyCode || 'USD', + paymentMethodTypes: paymentMethods || [], + countryCode: country, // Customer's country or base country of the store. + }; + + const elementsOptions = { + appearance: await initializeAppearance( api, elementLocation ), + fonts: getFontRulesFromPage(), + }; + + paymentMessageElement = api + .getStripe() + .elements( elementsOptions ) + .create( 'paymentMethodMessaging', options ); + paymentMessageElement.mount( '#payment-method-message' ); + } // This function converts relative units (rem/em) to pixels based on the current font size. function convertToPixels( value, baseFontSize ) { @@ -80,10 +114,14 @@ export const initializeBnplSiteMessaging = async () => { const priceElement = document.querySelector( '.price' ) || // For non-block product templates. document.querySelector( '.wp-block-woocommerce-product-price' ); // For block product templates. + const cartTotalElement = document.querySelector( + '.cart_totals .shop_table' + ); // Only attempt to adjust the margins if the price element is found. - if ( priceElement ) { - const style = window.getComputedStyle( priceElement ); + if ( priceElement || cartTotalElement ) { + const element = priceElement || cartTotalElement; + const style = window.getComputedStyle( element ); let bottomMargin = style.marginBottom; // Get the computed font size of the price element for 'em' calculations. @@ -111,6 +149,58 @@ export const initializeBnplSiteMessaging = async () => { document .getElementById( 'payment-method-message' ) .classList.add( 'ready' ); + + // On the cart page, get the height of the PMME after it's rendered and store it in a CSS variable. This helps + // prevent layout shifts when the PMME is loaded asynchronously upon cart total update. + if ( isCart ) { + // An element that won't be removed with the cart total update. + const cartCollaterals = document.querySelector( + '.cart-collaterals' + ); + const wcBnplHeight = getComputedStyle( cartCollaterals ) + .getPropertyValue( '--wc-bnpl-height' ) + .trim(); + + if ( wcBnplHeight ) { + return; + } + + const pmme = document.getElementById( + 'payment-method-message' + ); + const pmmeContainer = document.querySelector( + '.cart_totals .__PrivateStripeElement' + ); + setTimeout( () => { + const pmmeComputedStyle = window.getComputedStyle( pmme ); + const pmmeHeight = parseFloat( pmmeComputedStyle.height ); + const pmmeMarginBottom = parseFloat( bottomMargin ); + const pmmeTotalHeight = pmmeHeight + pmmeMarginBottom; + + const pmmeContainerComputedStyle = window.getComputedStyle( + pmmeContainer + ); + const pmmeContainerHeight = parseFloat( + pmmeContainerComputedStyle.height + ); + + cartCollaterals.style.setProperty( + '--wc-bnpl-height', + pmmeTotalHeight + 'px' + ); + cartCollaterals.style.setProperty( + '--wc-bnpl-container-height', + pmmeContainerHeight - 12 + 'px' + ); + + cartCollaterals.style.setProperty( + '--wc-bnpl-loader-margin', + pmmeMarginBottom + 2 + 'px' + ); + + pmme.style.setProperty( '--wc-bnpl-margin-bottom', '-4px' ); + }, 2000 ); + } } ); } diff --git a/client/product-details/bnpl-site-messaging/style.scss b/client/product-details/bnpl-site-messaging/style.scss index f74bb78a339..243ea0d777d 100644 --- a/client/product-details/bnpl-site-messaging/style.scss +++ b/client/product-details/bnpl-site-messaging/style.scss @@ -15,3 +15,44 @@ margin-bottom: var( --wc-bnpl-margin-bottom ); } } + +.cart_totals { + #payment-method-message { + margin: -8px 0 4px; + height: var( --wc-bnpl-height ); + padding: 2px 1em 0; + margin-bottom: var( --wc-bnpl-margin-bottom ); + + &.pmme-updated { + margin: -12px 0 0; + padding: 0 1em; + } + + &.skeleton { + margin-bottom: 4px; + background: #afafaf; + } + } + + .pmme-loading { + animation: pmme-loading 1s linear infinite alternate; + background: #afafaf; + height: var( --wc-bnpl-container-height ); + margin: -4px 1em var( --wc-bnpl-loader-margin ) 1em; + } +} + +@keyframes pmme-loading { + 0% { + background-color: hsl( 204, 10%, 90% ); + } + 100% { + background-color: hsl( 200, 20%, 95% ); + } +} + +.wc-block-components-totals-wrapper.slot-wrapper + .wc-block-components-bnpl-wrapper { + padding-left: 17px; + padding-right: 17px; +} diff --git a/client/product-details/index.js b/client/product-details/index.js index ef177740c5b..0591b84e794 100644 --- a/client/product-details/index.js +++ b/client/product-details/index.js @@ -5,6 +5,8 @@ * Internal dependencies */ import { initializeBnplSiteMessaging } from './bnpl-site-messaging'; +import request from 'wcpay/checkout/utils/request'; +import { buildAjaxURL } from 'wcpay/payment-request/utils'; jQuery( async function ( $ ) { /** @@ -18,15 +20,29 @@ jQuery( async function ( $ ) { * * If this variable is not set, the script will exit early to prevent further execution. */ - if ( ! window.wcpayStripeSiteMessaging ) { + if ( + ! window.wcpayStripeSiteMessaging || + window.wcpayStripeSiteMessaging.isCartBlock + ) { return; } - const { productVariations, productId } = window.wcpayStripeSiteMessaging; const { - amount: baseProductAmount = 0, - currency: productCurrency, - } = productVariations[ productId ]; + productVariations, + productId, + isCart, + } = window.wcpayStripeSiteMessaging; + + let baseProductAmount; + let productCurrency; + + if ( ! isCart ) { + const { amount, currency } = productVariations[ productId ]; + + baseProductAmount = amount || 0; + productCurrency = currency; + } + const QUANTITY_INPUT_SELECTOR = '.quantity input[type=number]'; const SINGLE_VARIATION_SELECTOR = '.single_variation_wrap'; const VARIATIONS_SELECTOR = '.variations'; @@ -62,11 +78,9 @@ jQuery( async function ( $ ) { const updateBnplPaymentMessage = ( amount, currency, quantity = 1 ) => { const totalAmount = parseIntOrReturnZero( amount ) * parseIntOrReturnZero( quantity ); - if ( totalAmount <= 0 || ! currency ) { return; } - bnplPaymentMessageElement.update( { amount: totalAmount, currency } ); }; @@ -83,6 +97,18 @@ jQuery( async function ( $ ) { ); }; + const bnplGetCartTotal = () => { + return request( + buildAjaxURL( + window.wcpayStripeSiteMessaging.wcAjaxUrl, + 'get_cart_total' + ), + { + security: window.wcpayStripeSiteMessaging.nonce, + } + ); + }; + // Update BNPL message based on the quantity change quantityInput.on( 'change', ( event ) => { let amount = baseProductAmount; @@ -99,6 +125,23 @@ jQuery( async function ( $ ) { updateBnplPaymentMessage( amount, productCurrency, event.target.value ); } ); + $( document.body ).on( 'updated_cart_totals', () => { + $( '#payment-method-message' ).before( + '
' + ); + $( '#payment-method-message' ).hide(); + bnplGetCartTotal().then( ( response ) => { + window.wcpayStripeSiteMessaging.cartTotal = response.total; + initializeBnplSiteMessaging().then( () => { + setTimeout( () => { + $( '.pmme-loading' ).remove(); + $( '#payment-method-message' ).show(); + $( '#payment-method-message' ).addClass( 'pmme-updated' ); + }, 1000 ); + } ); + } ); + } ); + // Handle BNPL messaging for variable products. if ( hasVariations ) { // Update BNPL message based on product variation diff --git a/client/settings/advanced-settings/multi-currency-toggle.js b/client/settings/advanced-settings/multi-currency-toggle.js index 7b81a869927..5a285c7c37a 100644 --- a/client/settings/advanced-settings/multi-currency-toggle.js +++ b/client/settings/advanced-settings/multi-currency-toggle.js @@ -40,7 +40,7 @@ const MultiCurrencyToggle = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx index b56f06a3d58..7cdc862f6e3 100644 --- a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx @@ -85,7 +85,7 @@ const MigrateAutomaticallyNotice: React.FC< Props > = ( { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx index 89a0005e954..34907e596ea 100644 --- a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx @@ -134,7 +134,7 @@ const MigrateOptionNotice: React.FC< Props > = ( { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-toggle.tsx b/client/settings/advanced-settings/stripe-billing-toggle.tsx index a340fabbc88..77671a71a66 100644 --- a/client/settings/advanced-settings/stripe-billing-toggle.tsx +++ b/client/settings/advanced-settings/stripe-billing-toggle.tsx @@ -56,10 +56,11 @@ const StripeBillingToggle: React.FC< Props > = ( { onChange } ) => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } + data-testid={ 'stripe-billing-toggle' } /> ); }; diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index 187feff74e1..c2b23f5968e 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -54,7 +54,7 @@ const WCPaySubscriptionsToggle = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/deposits/index.js b/client/settings/deposits/index.js index 3d9a7acdf82..97b26bd24ee 100644 --- a/client/settings/deposits/index.js +++ b/client/settings/deposits/index.js @@ -166,7 +166,7 @@ const DepositsSchedule = () => { learnMoreLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content @@ -189,7 +189,7 @@ const DepositsSchedule = () => { learnMoreLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content @@ -226,11 +226,15 @@ const Deposits = () => { ) }{ ' ' } + onClick={ () => { recordEvent( 'wcpay_settings_deposits_manage_in_stripe_click' - ) - } + ); + recordEvent( + 'wcpay_account_details_link_clicked', + { source: 'settings-deposits' } + ); + } } > { __( 'Manage in Stripe', 'woocommerce-payments' ) } diff --git a/client/settings/express-checkout-settings/woopay-settings.js b/client/settings/express-checkout-settings/woopay-settings.js index a6752896dc6..e577f7f0d7d 100644 --- a/client/settings/express-checkout-settings/woopay-settings.js +++ b/client/settings/express-checkout-settings/woopay-settings.js @@ -96,7 +96,7 @@ const WooPaySettings = ( { section } ) => { ), tosLink: ( @@ -117,7 +117,7 @@ const WooPaySettings = ( { section } ) => { ), }, @@ -230,7 +230,7 @@ const WooPaySettings = ( { section } ) => { /* eslint-enable prettier/prettier */ learnMoreLink: ( // eslint-disable-next-line max-len - + ), } } ) } diff --git a/client/settings/express-checkout/link-item.tsx b/client/settings/express-checkout/link-item.tsx index 56e7e2dfc79..ee3102affd6 100644 --- a/client/settings/express-checkout/link-item.tsx +++ b/client/settings/express-checkout/link-item.tsx @@ -158,7 +158,7 @@ const LinkExpressCheckoutItem = (): React.ReactElement => { target="_blank" rel="noreferrer" /* eslint-disable-next-line max-len */ - href="https://woo.com/document/woopayments/payment-methods/link-by-stripe/" + href="https://woocommerce.com/document/woopayments/payment-methods/link-by-stripe/" isSecondary > { __( diff --git a/client/settings/express-checkout/woopay-item.tsx b/client/settings/express-checkout/woopay-item.tsx index c5ed311f6a1..910cd930dff 100644 --- a/client/settings/express-checkout/woopay-item.tsx +++ b/client/settings/express-checkout/woopay-item.tsx @@ -128,7 +128,7 @@ const WooPayExpressCheckoutItem = (): React.ReactElement => { target="_blank" rel="noreferrer" // eslint-disable-next-line max-len - href="https://woo.com/document/woopay-merchant-documentation/" + href="https://woocommerce.com/document/woopay-merchant-documentation/" /> ), tosLink: ( @@ -149,7 +149,7 @@ const WooPayExpressCheckoutItem = (): React.ReactElement => { ), }, diff --git a/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx b/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx index af1dc265060..e3d1826640b 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx +++ b/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx @@ -47,7 +47,7 @@ const CVCVerificationRuleCard: React.FC = () => { target="_blank" type="external" // eslint-disable-next-line max-len - href="https://woo.com/document/woopayments/fraud-and-disputes/fraud-protection/#advanced-configuration" + href="https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/#advanced-configuration" /> ), }, diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap index c66a0a2b3d9..2d9cdd41659 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap @@ -189,7 +189,7 @@ exports[`CVC verification card renders correctly when CVC check is enabled 1`] = For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap index 931fa17bd69..abd09b5d4c7 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap @@ -981,7 +981,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -1981,7 +1981,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -2919,7 +2919,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -3776,7 +3776,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -4680,7 +4680,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -5500,7 +5500,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -6501,7 +6501,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -7421,7 +7421,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more diff --git a/client/settings/general-settings/index.js b/client/settings/general-settings/index.js index c8686f2e2a5..361ba3a8b11 100644 --- a/client/settings/general-settings/index.js +++ b/client/settings/general-settings/index.js @@ -60,7 +60,7 @@ const GeneralSettings = () => { target="_blank" rel="noreferrer" /* eslint-disable-next-line max-len */ - href="https://woo.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" + href="https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" /> ), learnMoreLink: ( @@ -68,7 +68,7 @@ const GeneralSettings = () => { ), }, @@ -112,7 +112,8 @@ const GeneralSettings = () => { ), }, @@ -124,7 +125,7 @@ const GeneralSettings = () => { { modalVisible && ( setModalVisible( false ) } + onClose={ () => setModalVisible( false ) } /> ) } { testModeModalVisible && ( diff --git a/client/settings/general-settings/test-mode-confirm-modal.tsx b/client/settings/general-settings/test-mode-confirm-modal.tsx index 21e62af1ac8..85185306c16 100644 --- a/client/settings/general-settings/test-mode-confirm-modal.tsx +++ b/client/settings/general-settings/test-mode-confirm-modal.tsx @@ -52,7 +52,7 @@ const TestModeConfirmationModal: React.FC< TestModeConfirmationModalProps > = (

{ __( 'Learn more about test mode', 'woocommerce-payments' ) } diff --git a/client/settings/settings-manager/index.js b/client/settings/settings-manager/index.js index ce0ff2f7f89..7afb5cbf55e 100644 --- a/client/settings/settings-manager/index.js +++ b/client/settings/settings-manager/index.js @@ -52,7 +52,7 @@ const ExpressCheckoutDescription = () => ( 'woocommerce-payments' ) }

- + { __( 'Learn more', 'woocommerce-payments' ) } @@ -83,7 +83,7 @@ const TransactionsDescription = () => ( 'woocommerce-payments' ) }

- + { __( 'View our documentation', 'woocommerce-payments' ) } @@ -104,7 +104,7 @@ const DepositsDescription = () => { depositDelayDays ) }

- + { __( 'Learn more about pending schedules', 'woocommerce-payments' @@ -124,7 +124,7 @@ const FraudProtectionDescription = () => { 'woocommerce-payments' ) }

- + { __( 'Learn more about fraud protection', 'woocommerce-payments' @@ -158,7 +158,7 @@ const AdvancedDescription = () => { 'woocommerce-payments' ) }

- + { __( 'View our documentation', 'woocommerce-payments' ) } diff --git a/client/settings/settings-warnings/incompatibility-notice.js b/client/settings/settings-warnings/incompatibility-notice.js index c86385b5b4f..f990abdec37 100644 --- a/client/settings/settings-warnings/incompatibility-notice.js +++ b/client/settings/settings-warnings/incompatibility-notice.js @@ -57,7 +57,7 @@ export const WooPayIncompatibilityNotice = () => ( 'One or more of your extensions are incompatible with WooPay.', 'woocommerce-payments' ) } - learnMoreLinkHref="https://woo.com/document/woopay-merchant-documentation/#compatibility" + learnMoreLinkHref="https://woocommerce.com/document/woopay-merchant-documentation/#compatibility" /> ); @@ -68,6 +68,6 @@ export const ExpressCheckoutIncompatibilityNotice = () => ( 'woocommerce-payments' ) } // eslint-disable-next-line max-len - learnMoreLinkHref="https://woo.com/document/woopayments/payment-methods/apple-pay-and-google-pay-compatibility/#faq-extra-fields-on-checkout" + learnMoreLinkHref="https://woocommerce.com/document/woopayments/payment-methods/apple-pay-and-google-pay-compatibility/#faq-extra-fields-on-checkout" /> ); diff --git a/client/settings/transactions/manual-capture-control.tsx b/client/settings/transactions/manual-capture-control.tsx index 5fef4bc564b..3fc20f383ac 100644 --- a/client/settings/transactions/manual-capture-control.tsx +++ b/client/settings/transactions/manual-capture-control.tsx @@ -70,7 +70,7 @@ const ManualCaptureControl = (): JSX.Element => { ), components: { a: ( - + ), }, } ) diff --git a/client/tos/modal/index.js b/client/tos/modal/index.js index c7369ec9ab1..5b5766e877c 100644 --- a/client/tos/modal/index.js +++ b/client/tos/modal/index.js @@ -73,7 +73,11 @@ const TosModalUI = ( { onAccept, onDecline, isBusy, hasError } ) => { { message }
- diff --git a/client/tracks/event.d.ts b/client/tracks/event.d.ts index fe6d99eb8ec..9f605763ac7 100644 --- a/client/tracks/event.d.ts +++ b/client/tracks/event.d.ts @@ -10,6 +10,7 @@ export type Event = | 'applepay_button_load' | 'page_view' | 'wcpay_connect_account_clicked' + | 'wcpay_account_details_link_clicked' | 'wcpay_welcome_learn_more' | 'wcpay_stripe_connected' | 'wcpay_connect_account_kyc_modal_opened' diff --git a/client/tracks/index.ts b/client/tracks/index.ts index 345da115cd8..5b459fde5fa 100644 --- a/client/tracks/index.ts +++ b/client/tracks/index.ts @@ -62,12 +62,10 @@ export const recordEvent = ( * * @param {string} eventName Name of the event. * @param {Object} [eventProperties] Event properties (optional). - * @param {boolean} isLegacy Event properties (optional). */ export const recordUserEvent = ( eventName: string, - eventProperties: Record< string, unknown > = {}, - isLegacy = false + eventProperties: Record< string, unknown > = {} ): void => { const nonce = getConfig( 'platformTrackerNonce' ) ?? @@ -80,7 +78,6 @@ export const recordUserEvent = ( body.append( 'action', 'platform_tracks' ); body.append( 'tracksEventName', eventName ); body.append( 'tracksEventProp', JSON.stringify( eventProperties ) ); - body.append( 'isLegacy', JSON.stringify( isLegacy ) ); // formData does not allow appending booleans, so we stringify it - it is parsed back to a boolean on the PHP side. fetch( ajaxUrl, { method: 'post', body, diff --git a/client/transactions/list/deposit.tsx b/client/transactions/list/deposit.tsx index 588f8f84142..b1889f3ad19 100644 --- a/client/transactions/list/deposit.tsx +++ b/client/transactions/list/deposit.tsx @@ -52,7 +52,7 @@ const Deposit: React.FC< DepositProps > = ( { depositId, dateAvailable } ) => { ), components: { learnMoreLink: ( - + ), }, } ) } diff --git a/client/transactions/list/index.tsx b/client/transactions/list/index.tsx index 7fedfcffcb1..a4b2108f308 100644 --- a/client/transactions/list/index.tsx +++ b/client/transactions/list/index.tsx @@ -155,11 +155,8 @@ const getColumns = ( }, { key: 'date', - label: __( 'Date / Time (UTC)', 'woocommerce-payments' ), - screenReaderLabel: __( - 'Date and time in UTC', - 'woocommerce-payments' - ), + label: __( 'Date / Time', 'woocommerce-payments' ), + screenReaderLabel: __( 'Date and time', 'woocommerce-payments' ), required: true, isLeftAligned: true, defaultOrder: 'desc', diff --git a/client/transactions/list/test/__snapshots__/index.tsx.snap b/client/transactions/list/test/__snapshots__/index.tsx.snap index 3e5c77a9a7a..1f737125544 100644 --- a/client/transactions/list/test/__snapshots__/index.tsx.snap +++ b/client/transactions/list/test/__snapshots__/index.tsx.snap @@ -248,19 +248,19 @@ exports[`Transactions list renders correctly when can filter by several currenci - Date and time in UTC + Date and time - Sort by Date and time in UTC in ascending order + Sort by Date and time in ascending order - Date / Time (UTC) + Date / Time - Date and time in UTC + Date and time - Sort by Date and time in UTC in ascending order + Sort by Date and time in ascending order - Date / Time (UTC) + Date / Time - Date and time in UTC + Date and time - Sort by Date and time in UTC in ascending order + Sort by Date and time in ascending order - Date / Time (UTC) + Date / Time - Date and time in UTC + Date and time - Sort by Date and time in UTC in ascending order + Sort by Date and time in ascending order - Date / Time (UTC) + Date / Time - Date and time in UTC + Date and time - Sort by Date and time in UTC in ascending order + Sort by Date and time in ascending order - Date / Time (UTC) + Date / Time - Date and time in UTC + Date and time - Sort by Date and time in UTC in ascending order + Sort by Date and time in ascending order { } ); test( 'sorts by default field date', () => { - sortBy( 'Date and time in UTC' ); + sortBy( 'Date and time' ); expectSortingToBe( 'date', 'asc' ); - sortBy( 'Date and time in UTC' ); + sortBy( 'Date and time' ); expectSortingToBe( 'date', 'desc' ); } ); @@ -620,7 +620,7 @@ describe( 'Transactions list', () => { const expected = [ '"Transaction Id"', - '"Date / Time (UTC)"', + '"Date / Time"', 'Type', 'Channel', '"Paid Currency"', diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 3893b13ab02..cdf1aeb9d04 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -18,9 +18,9 @@ import { PaymentMethod } from 'wcpay/types/payment-methods'; import { createInterpolateElement } from '@wordpress/element'; const countryFeeStripeDocsBaseLink = - 'https://woo.com/document/woopayments/fees-and-debits/fees/#'; + 'https://woocommerce.com/document/woopayments/fees-and-debits/fees/#'; const countryFeeStripeDocsBaseLinkNoCountry = - 'https://woo.com/document/woopayments/fees-and-debits/fees/'; + 'https://woocommerce.com/document/woopayments/fees-and-debits/fees/'; const countryFeeStripeDocsSectionNumbers: Record< string, string > = { AE: 'united-arab-emirates', AU: 'australia', @@ -76,7 +76,7 @@ const getStripeFeeSectionUrl = ( country: string ): string => { const getFeeDescriptionString = ( fee: BaseFee, - discountBasedMultiplier: number + discountBasedMultiplier = 1 ): string => { if ( fee.fixed_rate && fee.percentage_rate ) { return sprintf( @@ -122,14 +122,15 @@ export const formatMethodFeesTooltip = ( ? 1 - accountFees.discount[ 0 ].discount : 1; + // Per https://woo.com/es/terms-conditions/woopayments-promotion-2023/ we exclude FX fees from discounts. const total = { percentage_rate: - accountFees.base.percentage_rate + - accountFees.additional.percentage_rate + + accountFees.base.percentage_rate * discountAdjustedFeeRate + + accountFees.additional.percentage_rate * discountAdjustedFeeRate + accountFees.fx.percentage_rate, fixed_rate: - accountFees.base.fixed_rate + - accountFees.additional.fixed_rate + + accountFees.base.fixed_rate * discountAdjustedFeeRate + + accountFees.additional.fixed_rate * discountAdjustedFeeRate + accountFees.fx.fixed_rate, currency: accountFees.base.currency, }; @@ -165,12 +166,7 @@ export const formatMethodFeesTooltip = ( { hasFees( accountFees.fx ) ? (
Foreign exchange fee
-
- { getFeeDescriptionString( - accountFees.fx, - discountAdjustedFeeRate - ) } -
+
{ getFeeDescriptionString( accountFees.fx ) }
) : ( '' @@ -178,10 +174,7 @@ export const formatMethodFeesTooltip = (
Total per transaction
- { getFeeDescriptionString( - total, - discountAdjustedFeeRate - ) } + { getFeeDescriptionString( total ) }
{ wcpaySettings && @@ -267,10 +260,6 @@ export const formatAccountFeesDescription = ( fee: __( '%1$f%% + %2$s per transaction', 'woocommerce-payments' ), /* translators: %f percentage discount to apply */ discount: __( '(%f%% discount)', 'woocommerce-payments' ), - tc_link: __( - ' — see Terms and Conditions', - 'woocommerce-payments' - ), displayBaseFeeIfDifferent: true, ...customFormats, }; @@ -325,19 +314,6 @@ export const formatAccountFeesDescription = ( s: , }; - if ( discountFee.tc_url && 0 < formats.tc_link.length ) { - currentBaseFeeDescription += ' ' + formats.tc_link; - - conversionMap.tclink = ( - // eslint-disable-next-line jsx-a11y/anchor-has-content -
- ); - } - return createInterpolateElement( currentBaseFeeDescription, conversionMap diff --git a/client/utils/test/__snapshots__/account-fees.tsx.snap b/client/utils/test/__snapshots__/account-fees.tsx.snap index 5388a2b344d..2f4ff3a3fde 100644 --- a/client/utils/test/__snapshots__/account-fees.tsx.snap +++ b/client/utils/test/__snapshots__/account-fees.tsx.snap @@ -26,7 +26,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base Foreign exchange fee
- 0.8% + 1%
@@ -36,7 +36,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base
- 11.44% + $3.65 + 11.64% + $3.65
@@ -101,7 +101,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base > diff --git a/composer.json b/composer.json index dccb7b15259..945cf9db166 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "require-dev": { "composer/installers": "1.10.0", "phpunit/phpunit": "9.5.14", - "woocommerce/woocommerce-sniffs": "0.1.0", + "woocommerce/woocommerce-sniffs": "1.0.0", "woocommerce/action-scheduler": "3.1.6", "kalessil/production-dependencies-guard": "dev-master", "vimeo/psalm": "4.13.1", @@ -44,8 +44,9 @@ "automattic/jetpack-changelogger": "3.3.2", "spatie/phpunit-watcher": "1.23", "woocommerce/qit-cli": "0.4.0", - "slevomat/coding-standard": "8.0.0", - "dg/bypass-finals": "1.5.1" + "slevomat/coding-standard": "8.15.0", + "dg/bypass-finals": "1.5.1", + "sirbrillig/phpcs-variable-analysis": "^2.11" }, "scripts": { "test": [ diff --git a/composer.lock b/composer.lock index 863f69916ca..a09e313c36a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c77c6f448183a14790ec1fee9fc3636e", + "content-hash": "1c29170d1f90d9e5b4ff4d32b836b7da", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git", - "reference": "6ce7a1e1eba796643d7d32dc49057c7bb8e3233c" + "reference": "64748d02bf646e6b000f79dc8e4ea127bbd8df14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/6ce7a1e1eba796643d7d32dc49057c7bb8e3233c", - "reference": "6ce7a1e1eba796643d7d32dc49057c7bb8e3233c", + "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/64748d02bf646e6b000f79dc8e4ea127bbd8df14", + "reference": "64748d02bf646e6b000f79dc8e4ea127bbd8df14", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.0", + "automattic/jetpack-changelogger": "^4.1.1", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -52,9 +52,9 @@ ], "description": "Used to record internal usage stats for Automattic. Not visible to site owners.", "support": { - "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v2.0.0" + "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v2.0.1" }, - "time": "2023-11-20T20:02:34+00:00" + "time": "2024-03-12T22:00:11+00:00" }, { "name": "automattic/jetpack-admin-ui", @@ -114,24 +114,24 @@ }, { "name": "automattic/jetpack-assets", - "version": "v2.0.4", + "version": "v2.1.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-assets.git", - "reference": "ae8944abdb7a8da7137dedb9b4fe2afd81ed2d72" + "reference": "06614b6daf1229002ca80dae982421c74c351a83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/ae8944abdb7a8da7137dedb9b4fe2afd81ed2d72", - "reference": "ae8944abdb7a8da7137dedb9b4fe2afd81ed2d72", + "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/06614b6daf1229002ca80dae982421c74c351a83", + "reference": "06614b6daf1229002ca80dae982421c74c351a83", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2.0.0", + "automattic/jetpack-constants": "^2.0.1", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.5", + "automattic/jetpack-changelogger": "^4.1.1", "brain/monkey": "2.6.1", "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" @@ -148,7 +148,7 @@ "link-template": "https://github.com/Automattic/jetpack-assets/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "2.0.x-dev" + "dev-trunk": "2.1.x-dev" } }, "autoload": { @@ -165,9 +165,9 @@ ], "description": "Asset management utilities for Jetpack ecosystem packages", "support": { - "source": "https://github.com/Automattic/jetpack-assets/tree/v2.0.4" + "source": "https://github.com/Automattic/jetpack-assets/tree/v2.1.4" }, - "time": "2024-01-04T15:59:44+00:00" + "time": "2024-03-12T22:01:21+00:00" }, { "name": "automattic/jetpack-autoloader", @@ -333,23 +333,23 @@ }, { "name": "automattic/jetpack-constants", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", - "reference": "d4244e33d2d18902951af05ca5dbb689a23c9cdc" + "reference": "b159a8777450721a2e898a80930be1fdf59bae92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/d4244e33d2d18902951af05ca5dbb689a23c9cdc", - "reference": "d4244e33d2d18902951af05ca5dbb689a23c9cdc", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/b159a8777450721a2e898a80930be1fdf59bae92", + "reference": "b159a8777450721a2e898a80930be1fdf59bae92", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.0", + "automattic/jetpack-changelogger": "^4.1.1", "brain/monkey": "2.6.1", "yoast/phpunit-polyfills": "1.1.0" }, @@ -378,9 +378,9 @@ ], "description": "A wrapper for defining constants in a more testable way.", "support": { - "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.0" + "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.1" }, - "time": "2023-11-20T20:02:28+00:00" + "time": "2024-03-12T22:00:13+00:00" }, { "name": "automattic/jetpack-identity-crisis", @@ -444,23 +444,23 @@ }, { "name": "automattic/jetpack-ip", - "version": "v0.2.1", + "version": "v0.2.2", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-ip.git", - "reference": "2c4c7c237ae8628b64edbe920f6ceef9be15d7dc" + "reference": "b3efffb57901e236c3c1e7299b575563e1937013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/2c4c7c237ae8628b64edbe920f6ceef9be15d7dc", - "reference": "2c4c7c237ae8628b64edbe920f6ceef9be15d7dc", + "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/b3efffb57901e236c3c1e7299b575563e1937013", + "reference": "b3efffb57901e236c3c1e7299b575563e1937013", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.2", + "automattic/jetpack-changelogger": "^4.1.1", "brain/monkey": "2.6.1", "yoast/phpunit-polyfills": "1.1.0" }, @@ -493,29 +493,29 @@ ], "description": "Utilities for working with IP addresses.", "support": { - "source": "https://github.com/Automattic/jetpack-ip/tree/v0.2.1" + "source": "https://github.com/Automattic/jetpack-ip/tree/v0.2.2" }, - "time": "2023-11-21T18:58:12+00:00" + "time": "2024-03-12T22:00:05+00:00" }, { "name": "automattic/jetpack-logo", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-logo.git", - "reference": "21890dd130cae1365d6e59cf01db74e453e72d10" + "reference": "9b51eeedafcd024dd0eb74e2da0ab26d42ae4207" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-logo/zipball/21890dd130cae1365d6e59cf01db74e453e72d10", - "reference": "21890dd130cae1365d6e59cf01db74e453e72d10", + "url": "https://api.github.com/repos/Automattic/jetpack-logo/zipball/9b51eeedafcd024dd0eb74e2da0ab26d42ae4207", + "reference": "9b51eeedafcd024dd0eb74e2da0ab26d42ae4207", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.0", + "automattic/jetpack-changelogger": "^4.1.1", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -543,29 +543,29 @@ ], "description": "A logo for Jetpack", "support": { - "source": "https://github.com/Automattic/jetpack-logo/tree/v2.0.0" + "source": "https://github.com/Automattic/jetpack-logo/tree/v2.0.1" }, - "time": "2023-11-20T20:02:31+00:00" + "time": "2024-03-12T22:00:14+00:00" }, { "name": "automattic/jetpack-password-checker", - "version": "v0.3.0", + "version": "v0.3.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-password-checker.git", - "reference": "43120a1ddc032a9141ff02cc3ac7a7eac936d9f9" + "reference": "d9be23791e6f38debeacbf74174903f223ddfd89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/43120a1ddc032a9141ff02cc3ac7a7eac936d9f9", - "reference": "43120a1ddc032a9141ff02cc3ac7a7eac936d9f9", + "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/d9be23791e6f38debeacbf74174903f223ddfd89", + "reference": "d9be23791e6f38debeacbf74174903f223ddfd89", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.0", + "automattic/jetpack-changelogger": "^4.1.1", "automattic/wordbless": "@dev", "yoast/phpunit-polyfills": "1.1.0" }, @@ -595,30 +595,30 @@ ], "description": "Password Checker.", "support": { - "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.3.0" + "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.3.1" }, - "time": "2023-11-20T20:02:33+00:00" + "time": "2024-03-14T20:42:38+00:00" }, { "name": "automattic/jetpack-redirect", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-redirect.git", - "reference": "8f1bbfd4b046b8a0ae7b156007c2ef56a0ddbf76" + "reference": "24742b555faf49ec811e263653da898184c72366" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/8f1bbfd4b046b8a0ae7b156007c2ef56a0ddbf76", - "reference": "8f1bbfd4b046b8a0ae7b156007c2ef56a0ddbf76", + "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/24742b555faf49ec811e263653da898184c72366", + "reference": "24742b555faf49ec811e263653da898184c72366", "shasum": "" }, "require": { - "automattic/jetpack-status": "^2.0.0", + "automattic/jetpack-status": "^2.1.2", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.0", + "automattic/jetpack-changelogger": "^4.1.1", "brain/monkey": "2.6.1", "yoast/phpunit-polyfills": "1.1.0" }, @@ -647,29 +647,29 @@ ], "description": "Utilities to build URLs to the jetpack.com/redirect/ service", "support": { - "source": "https://github.com/Automattic/jetpack-redirect/tree/v2.0.0" + "source": "https://github.com/Automattic/jetpack-redirect/tree/v2.0.1" }, - "time": "2023-11-20T20:03:01+00:00" + "time": "2024-03-12T22:00:44+00:00" }, { "name": "automattic/jetpack-roles", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-roles.git", - "reference": "967e52052a17123a23f4112da3d8e7e995467cb2" + "reference": "a9b52f59a3c32b6413e6aa859a6a964ba969eec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/967e52052a17123a23f4112da3d8e7e995467cb2", - "reference": "967e52052a17123a23f4112da3d8e7e995467cb2", + "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/a9b52f59a3c32b6413e6aa859a6a964ba969eec9", + "reference": "a9b52f59a3c32b6413e6aa859a6a964ba969eec9", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.0", + "automattic/jetpack-changelogger": "^4.1.1", "brain/monkey": "2.6.1", "yoast/phpunit-polyfills": "1.1.0" }, @@ -698,31 +698,31 @@ ], "description": "Utilities, related with user roles and capabilities.", "support": { - "source": "https://github.com/Automattic/jetpack-roles/tree/v2.0.0" + "source": "https://github.com/Automattic/jetpack-roles/tree/v2.0.1" }, - "time": "2023-11-20T20:02:32+00:00" + "time": "2024-03-12T22:00:06+00:00" }, { "name": "automattic/jetpack-status", - "version": "v2.1.0", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-status.git", - "reference": "badaae2ef5345629f5333938e32a649bf946d688" + "reference": "a17f33f601303615f0b5e4cb9a23c0817243c68f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/badaae2ef5345629f5333938e32a649bf946d688", - "reference": "badaae2ef5345629f5333938e32a649bf946d688", + "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/a17f33f601303615f0b5e4cb9a23c0817243c68f", + "reference": "a17f33f601303615f0b5e4cb9a23c0817243c68f", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2.0.0", + "automattic/jetpack-constants": "^2.0.1", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.0.5", - "automattic/jetpack-ip": "^0.2.1", + "automattic/jetpack-changelogger": "^4.1.1", + "automattic/jetpack-ip": "^0.2.2", "brain/monkey": "2.6.1", "yoast/phpunit-polyfills": "1.1.0" }, @@ -751,9 +751,9 @@ ], "description": "Used to retrieve information about the current status of Jetpack and the site overall.", "support": { - "source": "https://github.com/Automattic/jetpack-status/tree/v2.1.0" + "source": "https://github.com/Automattic/jetpack-status/tree/v2.1.2" }, - "time": "2024-01-18T21:49:55+00:00" + "time": "2024-03-12T22:00:42+00:00" }, { "name": "automattic/jetpack-sync", @@ -1800,35 +1800,38 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.0", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", "phpcompatibility/php-compatibility": "^9.0", - "sensiolabs/security-checker": "^4.1.0" + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1841,6 +1844,10 @@ "email": "franck.nijhof@dealerdirect.com", "homepage": "http://www.frenck.nl", "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -1852,6 +1859,7 @@ "codesniffer", "composer", "installer", + "phpcbf", "phpcs", "plugin", "qa", @@ -1863,10 +1871,10 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2020-06-25T14:57:39+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "dg/bypass-finals", @@ -1958,6 +1966,53 @@ }, "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, { "name": "doctrine/instantiator", "version": "1.5.0", @@ -2521,20 +2576,21 @@ }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -2575,9 +2631,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -2839,16 +2901,16 @@ }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.0", + "version": "2.1.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "41bef18ba688af638b7310666db28e1ea9158b2f" + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/41bef18ba688af638b7310666db28e1ea9158b2f", - "reference": "41bef18ba688af638b7310666db28e1ea9158b2f", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", "shasum": "" }, "require": { @@ -2856,10 +2918,10 @@ "phpcompatibility/phpcompatibility-paragonie": "^1.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5" + "dealerdirect/phpcodesniffer-composer-installer": "^0.7" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -2883,13 +2945,180 @@ "compatibility", "phpcs", "standards", + "static analysis", "wordpress" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2019-08-28T14:22:28+00:00" + "time": "2022-10-24T09:00:36+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.0.9", + "squizlabs/php_codesniffer": "^3.8.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2023-12-08T16:49:07+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "908247bc65010c7b7541a9551e002db12e9dae70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/908247bc65010c7b7541a9551e002db12e9dae70", + "reference": "908247bc65010c7b7541a9551e002db12e9dae70", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.8.0 || 4.0.x-dev@dev" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2023-12-08T14:50:00+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3003,25 +3232,33 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" + "reference": "153ae662783729388a584b4361f2545e4d841e3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" }, "require-dev": { "ext-tokenizer": "*", - "psalm/phar": "^4.8" + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" }, "type": "library", "extra": { @@ -3047,30 +3284,30 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" }, - "time": "2022-03-15T21:29:03+00:00" + "time": "2024-02-23T11:10:43+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "d4f454f7e1193933f04e6500de3e79191648ed0c" + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d4f454f7e1193933f04e6500de3e79191648ed0c", - "reference": "d4f454f7e1193933f04e6500de3e79191648ed0c", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2 || ^2.0", "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0" + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { "phpspec/phpspec": "^6.0 || ^7.0", @@ -3116,22 +3353,22 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.18.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.19.0" }, - "time": "2023-12-07T16:22:33+00:00" + "time": "2024-02-29T11:52:51+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.26.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", "shasum": "" }, "require": { @@ -3163,22 +3400,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2024-02-23T16:05:55+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -3235,7 +3472,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -3243,7 +3480,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3885,16 +4122,16 @@ }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -3929,7 +4166,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -3937,7 +4174,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -4183,16 +4420,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -4237,7 +4474,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -4245,7 +4482,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -4312,16 +4549,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -4377,7 +4614,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -4385,20 +4622,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -4441,7 +4678,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -4449,7 +4686,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -4685,16 +4922,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -4706,7 +4943,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4727,8 +4964,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -4736,7 +4972,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -4847,44 +5083,102 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.11.17", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", + "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", + "sirbrillig/phpcs-import-detection": "^1.1", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], + "support": { + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + }, + "time": "2023-08-05T23:46:11+00:00" + }, { "name": "slevomat/coding-standard", - "version": "8.0.0", + "version": "8.15.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "84cc1693467996d0fc0838d4d4594eb9890ab295" + "reference": "7d1d957421618a3803b593ec31ace470177d7817" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/84cc1693467996d0fc0838d4d4594eb9890ab295", - "reference": "84cc1693467996d0fc0838d4d4594eb9890ab295", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.6.2", - "squizlabs/php_codesniffer": "^3.7.0" + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.9.0" }, "require-dev": { - "phing/phing": "2.17.3", + "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.7.14", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.1", - "phpstan/phpstan-strict-rules": "1.2.3", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" + "phpstan/phpstan": "1.10.60", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4892,9 +5186,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.0.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" }, "funding": [ { @@ -4906,7 +5204,7 @@ "type": "tidelift" } ], - "time": "2022-06-17T13:13:47+00:00" + "time": "2024-03-09T15:20:58+00:00" }, { "name": "spatie/phpunit-watcher", @@ -4972,16 +5270,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", "shasum": "" }, "require": { @@ -5048,20 +5346,20 @@ "type": "open_collective" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2024-02-16T15:06:51+00:00" }, { "name": "symfony/console", - "version": "v5.4.35", + "version": "v5.4.36", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931" + "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931", + "url": "https://api.github.com/repos/symfony/console/zipball/39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", + "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", "shasum": "" }, "require": { @@ -5131,7 +5429,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.35" + "source": "https://github.com/symfony/console/tree/v5.4.36" }, "funding": [ { @@ -5147,7 +5445,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:28:09+00:00" + "time": "2024-02-20T16:33:57+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5281,16 +5579,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { @@ -5304,9 +5602,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5343,7 +5638,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -5359,20 +5654,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", "shasum": "" }, "require": { @@ -5383,9 +5678,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5424,7 +5716,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" }, "funding": [ { @@ -5440,20 +5732,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", "shasum": "" }, "require": { @@ -5464,9 +5756,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5508,7 +5797,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" }, "funding": [ { @@ -5524,20 +5813,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -5551,9 +5840,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5591,7 +5877,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -5607,20 +5893,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", "shasum": "" }, "require": { @@ -5628,9 +5914,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5670,7 +5953,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" }, "funding": [ { @@ -5686,20 +5969,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -5707,9 +5990,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5753,7 +6033,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -5769,20 +6049,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/process", - "version": "v5.4.35", + "version": "v5.4.36", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cbc28e34015ad50166fc2f9c8962d28d0fe861eb" + "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cbc28e34015ad50166fc2f9c8962d28d0fe861eb", - "reference": "cbc28e34015ad50166fc2f9c8962d28d0fe861eb", + "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", + "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", "shasum": "" }, "require": { @@ -5815,7 +6095,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.35" + "source": "https://github.com/symfony/process/tree/v5.4.36" }, "funding": [ { @@ -5831,7 +6111,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-02-12T15:49:53+00:00" }, { "name": "symfony/service-contracts", @@ -5918,16 +6198,16 @@ }, { "name": "symfony/string", - "version": "v5.4.35", + "version": "v5.4.36", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2" + "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c209c4d0559acce1c9a2067612cfb5d35756edc2", - "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2", + "url": "https://api.github.com/repos/symfony/string/zipball/4e232c83622bd8cd32b794216aa29d0d266d353b", + "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b", "shasum": "" }, "require": { @@ -5984,7 +6264,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.35" + "source": "https://github.com/symfony/string/tree/v5.4.36" }, "funding": [ { @@ -6000,7 +6280,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-02-01T08:49:30+00:00" }, { "name": "symfony/yaml", @@ -6079,16 +6359,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -6117,7 +6397,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -6125,7 +6405,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "vimeo/psalm", @@ -6471,74 +6751,77 @@ }, { "name": "woocommerce/woocommerce-sniffs", - "version": "0.1.0", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-sniffs.git", - "reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79" + "reference": "3a65b917ff5ab5e65609e5dcb7bc62f9455bbef8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79", - "reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79", + "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/3a65b917ff5ab5e65609e5dcb7bc62f9455bbef8", + "reference": "3a65b917ff5ab5e65609e5dcb7bc62f9455bbef8", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "0.7.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", "php": ">=7.0", - "phpcompatibility/phpcompatibility-wp": "2.1.0", - "wp-coding-standards/wpcs": "2.3.0" + "phpcompatibility/phpcompatibility-wp": "^2.1.0", + "wp-coding-standards/wpcs": "^3.0.0" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Claudio Sanches", - "email": "claudio@automattic.com" - } - ], "description": "WooCommerce sniffs", "keywords": [ "phpcs", "standards", + "static analysis", "woocommerce", "wordpress" ], "support": { "issues": "https://github.com/woocommerce/woocommerce-sniffs/issues", - "source": "https://github.com/woocommerce/woocommerce-sniffs/tree/master" + "source": "https://github.com/woocommerce/woocommerce-sniffs/tree/1.0.0" }, - "time": "2020-08-06T18:23:45+00:00" + "time": "2023-09-29T13:52:33+00:00" }, { "name": "wp-coding-standards/wpcs", - "version": "2.3.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7da1894633f168fe244afc6de00d141f27517b62" + "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", - "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", + "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", "shasum": "" }, "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.3.1" + "phpcsstandards/phpcsextra": "^1.1.0", + "phpcsstandards/phpcsutils": "^1.0.8", + "squizlabs/php_codesniffer": "^3.7.2" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.0", - "phpcsstandards/phpcsdevtools": "^1.0", + "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -6555,6 +6838,7 @@ "keywords": [ "phpcs", "standards", + "static analysis", "wordpress" ], "support": { @@ -6562,7 +6846,13 @@ "source": "https://github.com/WordPress/WordPress-Coding-Standards", "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" }, - "time": "2020-05-13T23:57:56+00:00" + "funding": [ + { + "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", + "type": "custom" + } + ], + "time": "2023-09-14T07:06:09+00:00" }, { "name": "yoast/phpunit-polyfills", diff --git a/docs/rest-api/source/includes/wp-api-v3/deposits.md b/docs/rest-api/source/includes/wp-api-v3/deposits.md index 738a8289152..e63b3b0a060 100644 --- a/docs/rest-api/source/includes/wp-api-v3/deposits.md +++ b/docs/rest-api/source/includes/wp-api-v3/deposits.md @@ -354,7 +354,7 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/po_123abc \ ## Submit an instant deposit -Submit an instant deposit for a list of transactions. Only for eligible accounts. See [Instant Deposits with WooPayments](https://woo.com/document/woopayments/deposits/instant-deposits/) for more information. +Submit an instant deposit for a list of transactions. Only for eligible accounts. See [Instant Deposits with WooPayments](https://woocommerce.com/document/woopayments/deposits/instant-deposits/) for more information. ### HTTP request @@ -414,7 +414,7 @@ Request a CSV export of deposits matching the query. A link to the exported CSV curl -X POST 'https://example.com/wp-json/wc/v3/payments/deposits/download?status_is=paid' \ -u consumer_key:consumer_secret --data '{ - "user_email": "name@example.woo.com" + "user_email": "name@example.woocommerce.com" }' ``` diff --git a/docs/typescript/README.md b/docs/typescript/README.md index 43c3d05ba38..fc99689a793 100644 --- a/docs/typescript/README.md +++ b/docs/typescript/README.md @@ -8,6 +8,7 @@ This set of guidelines aims to make it easier for developers working on WooPayme - [How do I write React components in TypeScript?](./react-components.md) - [Where should I declare my types?](./declaring-types.md) - [Utility types and manipulation](./utility-and-manipulation.md) +- [General Conventions](./general-conventions.md) ## External resources diff --git a/docs/typescript/general-conventions.md b/docs/typescript/general-conventions.md new file mode 100644 index 00000000000..11ce04afffd --- /dev/null +++ b/docs/typescript/general-conventions.md @@ -0,0 +1,240 @@ +# General Conventions + +## Prefer named exports + +We should prefer named exports over default exports because they offer some advantages: + +- A predictable names used across the codebase. +- Better editor integration support (thanks to predictable names). + +### ✅ **Do** use named exports + +```ts +// exporter.ts +export const aValue = 42; +// importer.ts +import { aValue } from './exporter'; +``` + +### 👎 **Avoid** default exports + +```ts +// exporter.ts +export default 42; +// importer.ts +import iCanPutWhateverNameHere from './exporter'; +``` + +## Use JSDoc to provide relevant documentation + +JSDoc still has a place for highly-visible, relevant documentation. + +You can add documentation on types, interfaces, interface properties, enums and their members… Use +this, you'll thank yourself! + +```ts +interface Props { + /** Helpful detail for a confusing prop */ + whatIsThisFor?: number | Symbol; +} +``` + +### ✅ _Do_ use JSDoc to add descriptions and relevant information + +Adding descriptive JSDoc documentation is an excellent way to support ourselves as developers +working on the codebase. Editor integrations will put this documentation at your fingertips thanks +to the TypeScript language server! + +```ts +/** This is a very special number */ +const SPECIAL_NUMBER = 42; + +/** + * This is the K-combinator, also known as Kestrel. + * + * Purists will note the returned function should accept exactly 1 argument but instead we accept + * an arbitrary number. + * + * @param x The value that will be returned from the returned function + * @return A function that always returns the provided value + */ +function kestrel< T >( x: T ): ( ..._: unknown[] ) => T { + return () => x; +} +``` + +### ⛔️ _Don't_ add types in JSDoc + +This is redundant and better done with TypeScript syntax. + +```ts +/** + * Square a number. The types in this JSDoc are an example of what NOT to do. + * + * @param {number} n DO NOT INCLUDE THE {number} HERE! + * @return {number} n*n DO NOT INCLUDE THE {number} HERE! + */ +function square( n: number ): number { + return n * n; +} +``` + +## Rely on type inference + +TypeScript has a very powerful type system that can infer a lot from the code we write. Do not +annotate every type in the application, especially trivial things. Instead, rely on inference and +try to structure things in a way that empowers inference. Often, a few types go a long way. + +Try to keep the interfaces you do write close to their natural origin. For example, when you write a +`Props` interface put it in the same module as the component. + +## Avoid `any` + +The `any` type is dangerous and should be avoided. It makes it impossible for the type system to +discover issues or provide helpful information in many cases. + +`unknown` is a safer alternative to `any` that may be helpful. + +```ts +// ⛔️ any is dangerous. Do not use it! +const x: any = 0; +const y: string = x; // The type system thinks this is fine! 😱 + +// ✅ unknown is a good alternative to any. +const x2: unknown = produceSomeUntypedExternalValue(); +const y2: string = x2; // 😌 Type error. The type system correctly doesn't trust this to be a string. +``` + +## Avoid type assertions + +A _type assertion_ looks like this: `const x = 42 as string` or `const x = 42`, although the +latter form is less common due to the similar or ambiguous syntax with JSX. Some folks call these +"casts" or "coercion." + +Type assertions are very dangerous and should be avoided. They override the type system and tell it +that we know better. + +If you need to add a type to something, instead of an assertion try an _annotation_, like this: +`const x: number = 42`. This is help for TypeScript, it will use the annotated type but will also +check that it is valid. But don't annotate an assignment like this, TypeScript will infer it 🙂 + +## Use readonly + +We often rely on immutable data, we can encode this in the type system in a few ways. + +First, when defining an interface we can be clear about readonly properties to make our interface +immutable: + +```ts +interface CantTouchThis { + readonly string: string; + readonly array: ReadonlyArray< string >; + readonly tuple: readonly [ string, string ]; + readonly object: { + readonly prop: number; + }; +} +``` + +This also works in classes: + +```ts +class Klassy { + readonly immutable: string; + mutable: number; + + constructor() { + this.immutable = 'define it here, last chance'; + this.mutable = 0; + } + + tick() { + this.mutable += 1; // ✅ Ok + this.immutable = 'whoops'; // ⛔️ Type error, that's readonly! + } + + // Yes, these arrow functions are instance properties too and can be readonly 👀 + readonly noop = () => undefined; +} +``` + +And did you see that `ReadonlyArray`? That's a great alternative to `Array` or `T[]`. There +are also `ReadonlySet` and `ReadonlyMap` types! + +## Use const assertions + +`const` assertions are an excellent tool to prevent TS from _widening_ inferred types. This can sound a bit abstract so consider the following code: + +```ts +function getShapes() { + return [ + { kind: 'circle', radius: 100 }, + { kind: 'square', sideLength: 50 }, + ]; +} + +function useRadius( radius: number ) { + return radius; +} + +for ( const shape of getShapes() ) { + if ( shape.kind === 'circle' ) { + // TS still thinks shape can be any of the items returned from 'getShapes()' and thus (correctly) infers that 'shape.radius' may be 'undefined'. + useRadius( shape.radius ); // ⛔️ Can't pass 'number | undefined' when the function expects a 'number'. + } +} +``` + +`const` assertions allow us to get a concrete type without resorting to type guards or type assertion: + +```ts +function getShapes() { + return [ + { kind: 'circle', radius: 100 }, + { kind: 'square', sideLength: 50 }, + ] as const; // 💡 Add the const assertion here. +} + +function useRadius( radius: number ) { + return radius; +} + +for ( const shape of getShapes() ) { + if ( shape.kind === 'circle' ) { + useRadius( shape.radius ); // ✅ Ok, TS knows that if kind === 'circle' then 'shape' must have a 'radius' prop! + } +} +``` + +Not everything needs a _const assertion,_ but when we want to infer a readonly interface it's a +great option. Here's what that might look like: + +```ts +function actionMaker( direction: 'up' | 'down' ) { + return { type: 'GO', direction } as const; // Here is our const assertion: `as const` +} +``` + +This function has the following return type, inferred without writing a bunch of types. Perfect! +We've leveraged inference nicely: + +```ts +type ReturnType = { + readonly type: 'GO'; + readonly direction: 'up' | 'down'; +}; +``` + +## Avoid enums + +An [enum](https://www.typescriptlang.org/docs/handbook/enums.html) represents a set of named constants. Avoid using enums in TypeScript due to their [problematic tradeoffs](https://blog.logrocket.com/why-typescript-enums-suck/) – enum values are not type-safe, exhibit surprising behavior/have volatile values and can lead to runtime errors. + +Consider using string unions or objects with const assertions instead. + +- Instead of a simple enum `enum Status { On, Off }` use a string union: + + `type Status = 'on' | 'off';` + +- If you really need an object, use an object with a const assertion: + + `const Status = { On: 'on', Off: 'off' } as const;` diff --git a/docs/version-support-policy.md b/docs/version-support-policy.md index 893fe69b09e..f55231ad81f 100644 --- a/docs/version-support-policy.md +++ b/docs/version-support-policy.md @@ -1,6 +1,6 @@ # Version Support Policy -We have officially announced the L-2 version support policy for WooCommerce and WordPress core [since May 2021](https://developer.woo.com/2021/05/12/woocommerce-payments-is-adopting-a-new-version-support-policy/). +We have officially announced the L-2 version support policy for WooCommerce and WordPress core [since May 2021](https://developer.woocommerce.com/2021/05/12/woocommerce-payments-is-adopting-a-new-version-support-policy/). However, in the practice, we're really enforcing only WordPress core with the `Requires at least` in [`readme.txt`](https://github.com/Automattic/woocommerce-payments/blob/develop/readme.txt) and [`woocommerce-payments.php`](https://github.com/Automattic/woocommerce-payments/blob/develop/woocommerce-payments.php). diff --git a/includes/admin/class-wc-payments-admin-settings.php b/includes/admin/class-wc-payments-admin-settings.php index 49a062a57ee..cec7ca48b17 100644 --- a/includes/admin/class-wc-payments-admin-settings.php +++ b/includes/admin/class-wc-payments-admin-settings.php @@ -57,7 +57,7 @@ public function display_test_mode_notice() {

WC_Payments_Utils::get_language_data( get_locale() ), 'trackingInfo' => $this->account->get_tracking_info(), + 'lifetimeTPV' => $this->account->get_lifetime_total_payments_volume(), ]; return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); @@ -1219,7 +1220,7 @@ public function add_transactions_notification_badge() { * @return int The number of disputes which need a response. */ private function get_disputes_awaiting_response_count() { - $send_callback = function() { + $send_callback = function () { $request = Request::get( WC_Payments_API_Client::DISPUTES_API . '/status_counts' ); $request->assign_hook( 'wcpay_get_dispute_status_counts' ); return $request->send(); @@ -1249,7 +1250,7 @@ private function get_uncaptured_transactions_count() { $test_mode = WC_Payments::mode()->is_test(); $cache_key = $test_mode ? DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE : DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY; - $send_callback = function() { + $send_callback = function () { $request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API . '/summary' ); $request->assign_hook( 'wc_pay_get_authorizations_summary' ); return $request->send(); diff --git a/includes/admin/class-wc-payments-bnpl-announcement.php b/includes/admin/class-wc-payments-bnpl-announcement.php new file mode 100644 index 00000000000..8fca5c3d561 --- /dev/null +++ b/includes/admin/class-wc-payments-bnpl-announcement.php @@ -0,0 +1,209 @@ +gateway = $gateway; + $this->account = $account; + $this->current_time = $current_time; + } + + /** + * Initializes this class's WP hooks. + * + * @return void + */ + public function init_hooks() { + add_action( 'current_screen', [ $this, 'maybe_enqueue_scripts' ] ); + } + + /** + * Needs to run after `current_screen`, to determine which page we're currently on. + * + * @return void + */ + public function maybe_enqueue_scripts() { + if ( ! is_admin() ) { + return; + } + + // Only shown once to each Administrator and Shop Manager users. + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + // Time boxed - Campaign expires after 90 days. + if ( $this->current_time > strtotime( '2024-07-15 00:00:00' ) ) { + return; + } + + if ( get_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed', true ) === '1' ) { + return; + } + + // Only displayed to BNPL eligible countries - AU, NZ, US, AT, BE, CA, CZ, DK, FI, FR, DE, GR, IE, IT, NO, PL, PT, ES, SE, CH, NL, UK, US. + if ( ! in_array( + $this->account->get_account_country(), + [ + \WCPay\Constants\Country_Code::AUSTRALIA, + \WCPay\Constants\Country_Code::AUSTRIA, + \WCPay\Constants\Country_Code::NEW_ZEALAND, + \WCPay\Constants\Country_Code::UNITED_STATES, + \WCPay\Constants\Country_Code::BELGIUM, + \WCPay\Constants\Country_Code::CANADA, + \WCPay\Constants\Country_Code::CZECHIA, + \WCPay\Constants\Country_Code::DENMARK, + \WCPay\Constants\Country_Code::FINLAND, + \WCPay\Constants\Country_Code::FRANCE, + \WCPay\Constants\Country_Code::GERMANY, + \WCPay\Constants\Country_Code::GREECE, + \WCPay\Constants\Country_Code::IRELAND, + \WCPay\Constants\Country_Code::ITALY, + \WCPay\Constants\Country_Code::NORWAY, + \WCPay\Constants\Country_Code::POLAND, + \WCPay\Constants\Country_Code::PORTUGAL, + \WCPay\Constants\Country_Code::SPAIN, + \WCPay\Constants\Country_Code::SWEDEN, + \WCPay\Constants\Country_Code::SWITZERLAND, + \WCPay\Constants\Country_Code::NETHERLANDS, + \WCPay\Constants\Country_Code::UNITED_KINGDOM, + ], + true + ) ) { + return; + } + + // just to be safe for older versions. + if ( ! class_exists( '\Automattic\WooCommerce\Admin\PageController' ) ) { + return; + } + + // Target page to be displayed on - Any WooPayments page except disputes. + $current_page = \Automattic\WooCommerce\Admin\PageController::get_instance()->get_current_page(); + if ( ! WC_Payments_Utils::is_payments_settings_page() && ( empty( $current_page ) || ! in_array( + $current_page['id'], + [ + 'wc-payments', + 'wc-payments-deposits', + 'wc-payments-transactions', + 'wc-payments-deposit-details', + 'wc-payments-transaction-details', + 'wc-payments-multi-currency-setup', + ], + true + ) ) ) { + return; + } + + // at least 3 purchases (on any payment method). + $woopayments_successful_orders_count = $this->get_woopayments_successful_orders_count(); + if ( $woopayments_successful_orders_count < 3 ) { + return; + } + + // don't display the promo if the merchant already has BNPL methods enabled. + $enabled_bnpl_payment_methods = array_intersect( + \WCPay\Constants\Payment_Method::BNPL_PAYMENT_METHODS, + $this->gateway->get_upe_enabled_payment_method_ids() + ); + if ( ! empty( $enabled_bnpl_payment_methods ) ) { + return; + } + + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + + add_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed', '1' ); + } + + /** + * Enqueues the script & styles for the BNPL announcement dialog. + * + * @return void + */ + public function enqueue_scripts() { + WC_Payments::register_script_with_dependencies( 'WCPAY_BNPL_ANNOUNCEMENT', 'dist/bnpl-announcement' ); + wp_set_script_translations( 'WCPAY_BNPL_ANNOUNCEMENT', 'woocommerce-payments' ); + WC_Payments_Utils::register_style( + 'WCPAY_BNPL_ANNOUNCEMENT', + plugins_url( 'dist/bnpl-announcement.css', WCPAY_PLUGIN_FILE ), + [ 'wc-components' ], + WC_Payments::get_file_version( 'dist/bnpl-announcement.css' ), + 'all' + ); + // conditionally show afterpay/clearpay based on account country. + $wcpay_bnpl_announcement = rawurlencode( wp_json_encode( [ 'accountCountry' => $this->account->get_account_country() ] ) ); + wp_add_inline_script( + 'WCPAY_BNPL_ANNOUNCEMENT', + " + var wcpayBnplAnnouncement = wcpayBnplAnnouncement || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_bnpl_announcement ) . "' ) ); + ", + 'before' + ); + + wp_enqueue_script( 'WCPAY_BNPL_ANNOUNCEMENT' ); + wp_enqueue_style( 'WCPAY_BNPL_ANNOUNCEMENT' ); + } + + /** + * Returns the number of successful orders paid with any WooPayments payment method. + * + * @return int + */ + private function get_woopayments_successful_orders_count() { + // using a transient to store the value of a previous calculation, since it can be expensive on each page load. + $wcpay_successful_orders_count = get_transient( 'wcpay_bnpl_april15_successful_purchases_count' ); + if ( false !== $wcpay_successful_orders_count ) { + return intval( $wcpay_successful_orders_count ); + } + + $orders = wc_get_orders( + [ + // we don't need them all, just more than 3. + 'limit' => 5, + 'status' => [ 'completed', 'processing' ], + ] + ); + $orders_count = count( $orders ); + + // storing the transient for a couple of days is probably sufficient, in case the value is too low (less than 3). + set_transient( 'wcpay_bnpl_april15_successful_purchases_count', $orders_count, 2 * DAY_IN_SECONDS ); + + return $orders_count; + } +} diff --git a/includes/admin/class-wc-rest-payments-capital-controller.php b/includes/admin/class-wc-rest-payments-capital-controller.php index b1635dfeb63..056156011b3 100644 --- a/includes/admin/class-wc-rest-payments-capital-controller.php +++ b/includes/admin/class-wc-rest-payments-capital-controller.php @@ -62,5 +62,4 @@ public function get_loans() { $request->assign_hook( 'wcpay_get_loans_request' ); return $request->handle_rest_request(); } - } diff --git a/includes/admin/class-wc-rest-payments-customer-controller.php b/includes/admin/class-wc-rest-payments-customer-controller.php index e5a109e9d20..c0f43a97003 100644 --- a/includes/admin/class-wc-rest-payments-customer-controller.php +++ b/includes/admin/class-wc-rest-payments-customer-controller.php @@ -277,5 +277,4 @@ public function get_item_schema() { ], ]; } - } diff --git a/includes/admin/class-wc-rest-payments-files-controller.php b/includes/admin/class-wc-rest-payments-files-controller.php index db41002e1df..e66eedefffd 100644 --- a/includes/admin/class-wc-rest-payments-files-controller.php +++ b/includes/admin/class-wc-rest-payments-files-controller.php @@ -62,7 +62,6 @@ public function register_routes() { 'permission_callback' => [], ] ); - } /** @@ -118,7 +117,7 @@ public function get_file( WP_REST_Request $request ) { */ add_filter( 'rest_pre_serve_request', - function ( bool $served, WP_HTTP_Response $response ) : bool { + function ( bool $served, WP_HTTP_Response $response ): bool { echo $response->get_data(); // @codingStandardsIgnoreLine return true; }, @@ -134,7 +133,6 @@ function ( bool $served, WP_HTTP_Response $response ) : bool { 'Content-Disposition' => 'inline', ] ); - } /** @@ -191,7 +189,7 @@ public function get_file_content( WP_REST_Request $request ) { * * @return WP_Error */ - private function file_error_response( WP_Error $error ) : WP_Error { + private function file_error_response( WP_Error $error ): WP_Error { $error_status_code = 'resource_missing' === $error->get_error_code() ? WP_Http::NOT_FOUND : WP_Http::INTERNAL_SERVER_ERROR; return new WP_Error( $error->get_error_code(), diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index e23f1ebfcff..093004d37b2 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -418,7 +418,7 @@ public function create_terminal_intent( $request ) { * @return array|null * @throws \Exception */ - public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ) :array { + public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ): array { $payment_methods = $request->get_param( 'payment_methods' ); if ( null === $payment_methods ) { return $default_value; @@ -446,7 +446,7 @@ public function get_terminal_intent_payment_method( $request, array $default_val * @return string|null * @throws \Exception */ - public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ) : string { + public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ): string { $capture_method = $request->get_param( 'capture_method' ); if ( null === $capture_method ) { return $default_value; diff --git a/includes/admin/class-wc-rest-payments-payment-intents-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-controller.php index 53d02e8afa3..670d15a3089 100644 --- a/includes/admin/class-wc-rest-payments-payment-intents-controller.php +++ b/includes/admin/class-wc-rest-payments-payment-intents-controller.php @@ -51,5 +51,4 @@ public function get_payment_intent( $request ) { return $this->forward_request( 'get_intent', [ $payment_intent_id ] ); } - } diff --git a/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php index 882cda6ab5b..972717724a8 100644 --- a/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php +++ b/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php @@ -371,6 +371,4 @@ public function prepare_item_for_response( $item, $request ) { return rest_ensure_response( $prepared_item ); } - - } diff --git a/includes/admin/class-wc-rest-payments-settings-controller.php b/includes/admin/class-wc-rest-payments-settings-controller.php index c20fcd80208..9137df7062d 100644 --- a/includes/admin/class-wc-rest-payments-settings-controller.php +++ b/includes/admin/class-wc-rest-payments-settings-controller.php @@ -563,7 +563,7 @@ public function update_settings( WP_REST_Request $request ) { return new WP_REST_Response( [ 'server_error' => $update_account_result->get_error_message() ], 400 ); } - return new WP_REST_Response( [], 200 ); + return new WP_REST_Response( $this->get_settings(), 200 ); } /** diff --git a/includes/admin/class-wc-rest-user-exists-controller.php b/includes/admin/class-wc-rest-user-exists-controller.php deleted file mode 100644 index bd9d1eeb6aa..00000000000 --- a/includes/admin/class-wc-rest-user-exists-controller.php +++ /dev/null @@ -1,89 +0,0 @@ -namespace, - '/' . $this->rest_base, - // Silence the nosemgrep audit rule because this controller (and its routes) is not being used. - // This file is only left to avoid plugin upgrade errors. - // See this issue for a permanent fix: https://github.com/Automattic/woocommerce-payments/issues/6304 - // nosemgrep: audit.php.wp.security.rest-route.permission-callback.return-true -- reason: this controller is not being used. - [ - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'user_exists' ], - 'permission_callback' => '__return_true', - 'args' => [ - 'email' => [ - 'required' => true, - 'description' => __( 'Email address.', 'woocommerce-payments' ), - 'type' => 'string', - 'format' => 'email', - ], - ], - ] - ); - } - - /** - * Retrieve if a user exists by email address. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_REST_Response - */ - public function user_exists( WP_REST_Request $request ): WP_REST_Response { - $email = $request->get_param( 'email' ); - $email_exists = ! empty( email_exists( $email ) ); - $message = null; - - if ( $email_exists ) { - // Use this function to show the core error message. - $error = wc_create_new_customer( $email ); - $message = $error->get_error_message(); - } - - return new WP_REST_Response( - [ - 'user-exists' => $email_exists, - 'message' => $message, - ] - ); - } -} - diff --git a/includes/admin/class-wc-rest-woopay-session-controller.php b/includes/admin/class-wc-rest-woopay-session-controller.php index 3dd0fd8b1f7..24eb0b827d2 100644 --- a/includes/admin/class-wc-rest-woopay-session-controller.php +++ b/includes/admin/class-wc-rest-woopay-session-controller.php @@ -93,4 +93,3 @@ private function is_request_from_woopay(): bool { return isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT']; } } - diff --git a/includes/admin/tasks/class-wc-payments-task-disputes.php b/includes/admin/tasks/class-wc-payments-task-disputes.php index abc24c5df95..b7212ec7623 100644 --- a/includes/admin/tasks/class-wc-payments-task-disputes.php +++ b/includes/admin/tasks/class-wc-payments-task-disputes.php @@ -207,7 +207,6 @@ public function get_additional_info() { ), count( (array) $this->disputes_due_within_7d ) ); - } /** @@ -325,7 +324,7 @@ private function get_disputes_needing_response_within_days( $num_days ) { private function get_disputes_needing_response() { return $this->database_cache->get_or_add( Database_Cache::ACTIVE_DISPUTES_KEY, - function() { + function () { $response = $this->api_client->get_disputes( [ 'pagesize' => 50, @@ -338,7 +337,7 @@ function() { // sort by due_by date ascending. usort( $active_disputes, - function( $a, $b ) { + function ( $a, $b ) { $a_due_by = new \DateTime( $a['due_by'] ); $b_due_by = new \DateTime( $b['due_by'] ); diff --git a/includes/admin/tracks/tracks-loader.php b/includes/admin/tracks/tracks-loader.php index 21fcb1278e1..93ebbe2049e 100644 --- a/includes/admin/tracks/tracks-loader.php +++ b/includes/admin/tracks/tracks-loader.php @@ -24,7 +24,7 @@ function record_tracker_events() { // Loaded on admin_init to ensure that we are in admin and that WC_Tracks is loaded. add_action( 'admin_init', - function() { + function () { if ( ! class_exists( 'WC_Tracks' ) ) { return; } diff --git a/includes/class-database-cache.php b/includes/class-database-cache.php index 6c6cdfd3344..68b4c7d4d89 100644 --- a/includes/class-database-cache.php +++ b/includes/class-database-cache.php @@ -185,6 +185,9 @@ public function add( string $key, $data ) { */ public function delete( string $key ) { delete_option( $key ); + + // Clear WP Cache to ensure the new data is fetched by other processes. + wp_cache_delete( $key, 'options' ); } /** @@ -297,11 +300,10 @@ private function write_to_cache( string $key, $data, bool $errored ): array { $cache_contents['errored'] = $errored; // Create or update the option cache. - if ( false === get_option( $key ) ) { - add_option( $key, $cache_contents, '', 'no' ); - } else { - update_option( $key, $cache_contents, 'no' ); - } + update_option( $key, $cache_contents, 'no' ); + + // Clear WP Cache to ensure the new data is fetched by other processes. + wp_cache_delete( $key, 'options' ); return $cache_contents; } diff --git a/includes/class-duplicate-payment-prevention-service.php b/includes/class-duplicate-payment-prevention-service.php index 35ac1896234..50a704793d1 100644 --- a/includes/class-duplicate-payment-prevention-service.php +++ b/includes/class-duplicate-payment-prevention-service.php @@ -96,7 +96,7 @@ public function check_payment_intent_attached_to_order_succeeded( WC_Order $orde } catch ( Exception $e ) { Logger::error( 'Failed to fetch attached payment intent: ' . $e ); return; - }; + } if ( ! $intent->is_authorized() ) { return; diff --git a/includes/class-experimental-abtest.php b/includes/class-experimental-abtest.php index fbbe80775c3..cf7e785656c 100644 --- a/includes/class-experimental-abtest.php +++ b/includes/class-experimental-abtest.php @@ -170,4 +170,3 @@ protected function request_variation( $test_name ) { return $get; } } - diff --git a/includes/class-payment-information.php b/includes/class-payment-information.php index 1f95a05887d..38191bf7ded 100644 --- a/includes/class-payment-information.php +++ b/includes/class-payment-information.php @@ -134,7 +134,7 @@ public function __construct( if ( empty( $payment_method ) && empty( $token ) && ! \WC_Payments::is_network_saved_cards_enabled() ) { // If network-wide cards are enabled, a payment method or token may not be specified and the platform default one will be used. throw new Invalid_Payment_Method_Exception( - __( 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.', 'woocommerce-payments' ), + esc_html__( 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.', 'woocommerce-payments' ), 'payment_method_not_provided' ); } diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 75a318ba7e3..7ca12d90326 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -118,9 +118,13 @@ class WC_Payment_Gateway_WCPay extends WC_Payment_Gateway_CC { const UPE_APPEARANCE_TRANSIENT = 'wcpay_upe_appearance'; const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wcpay_wc_blocks_upe_appearance'; const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance'; + const UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_classic_cart_appearance'; + const UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_cart_block_appearance'; const UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_appearance_theme'; const WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_wc_blocks_upe_appearance_theme'; const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance_theme'; + const UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_classic_cart_appearance_theme'; + const UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_cart_block_appearance_theme'; /** * Client for making requests to the WooCommerce Payments API @@ -308,7 +312,7 @@ public function __construct( 'title' => __( 'Customer bank statement', 'woocommerce-payments' ), 'description' => WC_Payments_Utils::esc_interpolated_html( __( 'Edit the way your store name appears on your customers’ bank statements (read more about requirements here).', 'woocommerce-payments' ), - [ 'a' => '' ] + [ 'a' => '' ] ), ], 'manual_capture' => [ @@ -527,6 +531,8 @@ public function init_hooks() { add_action( 'woocommerce_update_order', [ $this, 'schedule_order_tracking' ], 10, 2 ); add_filter( 'rest_request_before_callbacks', [ $this, 'remove_all_actions_on_preflight_check' ], 10, 3 ); + + add_action( 'woocommerce_settings_save_general', [ $this, 'update_fraud_rules_based_on_general_options' ], 20 ); } $this->maybe_init_subscriptions_hooks(); @@ -1999,7 +2005,7 @@ public function get_payment_method_to_use_for_intent() { * @param Payment_Information $payment_information Payment information object for transaction. * @return array List of payment methods. */ - public function get_payment_method_types( $payment_information ) : array { + public function get_payment_method_types( $payment_information ): array { $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification $token = $payment_information->get_payment_token(); @@ -2972,6 +2978,48 @@ protected function maybe_refresh_fraud_protection_settings() { } } + /** + * Updates the fraud rules depending on some settings when those settings have changed. + * + * @return void This is a readonly action. + */ + public function update_fraud_rules_based_on_general_options() { + // If the protection level is not "advanced", no need to run this, because it won't contain the IP country filter. + if ( 'advanced' !== $this->get_current_protection_level() ) { + return; + } + + // If the ruleset can't be parsed, skip updating. + $ruleset = $this->get_advanced_fraud_protection_settings(); + if ( + 'error' === $ruleset + || ! is_array( $ruleset ) + || ! Fraud_Risk_Tools::is_valid_ruleset_array( $ruleset ) + ) { + return; + } + + $needs_update = false; + foreach ( $ruleset as &$rule_array ) { + if ( isset( $rule_array['key'] ) && Fraud_Risk_Tools::RULE_INTERNATIONAL_IP_ADDRESS === $rule_array['key'] ) { + $new_rule_array = Fraud_Risk_Tools::get_international_ip_address_rule()->to_array(); + if ( isset( $rule_array['check'] ) + && isset( $new_rule_array['check'] ) + && wp_json_encode( $rule_array['check'] ) !== wp_json_encode( $new_rule_array['check'] ) + ) { + $rule_array = $new_rule_array; + $needs_update = true; + } + } + } + + // Update the possibly changed values on the server, and the transient. + if ( $needs_update ) { + $this->payments_api_client->save_fraud_ruleset( $ruleset ); + set_transient( 'wcpay_fraud_protection_settings', $ruleset, DAY_IN_SECONDS ); + } + } + /** * The get_icon() method from the WC_Payment_Gateway class wraps the icon URL into a prepared HTML element, but there are situations when this * element needs to be rendered differently on the UI (e.g. additional styles or `display` property). @@ -3813,7 +3861,7 @@ public function get_payment_method_ids_enabled_at_checkout( $order_id = null, $f in_array( Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $enabled_payment_methods, true ) ) { $enabled_payment_methods = array_filter( $enabled_payment_methods, - static function( $method ) { + static function ( $method ) { return Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID !== $method; } ); @@ -3887,7 +3935,7 @@ public function save_upe_appearance_ajax() { $elements_location = isset( $_POST['elements_location'] ) ? wc_clean( wp_unslash( $_POST['elements_location'] ) ) : null; $appearance = isset( $_POST['appearance'] ) ? json_decode( wc_clean( wp_unslash( $_POST['appearance'] ) ) ) : null; - $valid_locations = [ 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page' ]; + $valid_locations = [ 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page', 'bnpl_classic_cart', 'bnpl_cart_block' ]; if ( ! $elements_location || ! in_array( $elements_location, $valid_locations, true ) ) { throw new Exception( __( 'Unable to update UPE appearance values at this time.', 'woocommerce-payments' ) @@ -3909,7 +3957,7 @@ public function save_upe_appearance_ajax() { /** * This filter is only called on "save" of the appearance, to avoid calling it on every page load. * If you apply changes through this filter, you'll need to clear the transient data to see them at checkout. - * $elements_location can be 'blocks_checkout', 'shortcode_checkout', or 'bnpl_product_page'. + * $elements_location can be 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page', 'bnpl_classic_cart', 'bnpl_cart_block'. * * @since 7.4.0 */ @@ -3919,11 +3967,15 @@ public function save_upe_appearance_ajax() { 'shortcode_checkout' => self::UPE_APPEARANCE_TRANSIENT, 'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT, 'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT, + 'bnpl_classic_cart' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT, + 'bnpl_cart_block' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT, ][ $elements_location ]; $appearance_theme_transient = [ 'shortcode_checkout' => self::UPE_APPEARANCE_THEME_TRANSIENT, 'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT, 'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT, + 'bnpl_classic_cart' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT, + 'bnpl_cart_block' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT, ][ $elements_location ]; if ( null !== $appearance ) { @@ -3952,9 +4004,13 @@ public function clear_upe_appearance_transient() { delete_transient( self::UPE_APPEARANCE_TRANSIENT ); delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT ); + delete_transient( self::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT ); + delete_transient( self::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT ); delete_transient( self::UPE_APPEARANCE_THEME_TRANSIENT ); delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT ); delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT ); + delete_transient( self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT ); + delete_transient( self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT ); } /** @@ -4210,38 +4266,23 @@ public function get_theme_icon() { * @return string */ public function get_method_description() { - $description_links = [ - 'br' => '
', - 'tosLink' => '
', - 'privacyLink' => '', - 'woopayMechantTosLink' => '', - ]; - - $description = WC_Payments_Utils::esc_interpolated_html( - sprintf( - /* translators: %1$s: WooPayments, tosLink: Link to terms of service page, privacyLink: Link to privacy policy page */ - __( - '%1$s gives your store flexibility to accept credit cards, debit cards, and Apple Pay. Enable popular local payment methods and other digital wallets like Google Pay to give customers even more choice.

- By using %1$s you agree to be bound by our Terms of Service and acknowledge that you have read our Privacy Policy', - 'woocommerce-payments' - ), - 'WooPayments' + $description = sprintf( + /* translators: %1$s: WooPayments */ + __( + '%1$s gives your store flexibility to accept credit cards, debit cards, and Apple Pay. Enable popular local payment methods and other digital wallets like Google Pay to give customers even more choice.', + 'woocommerce-payments' ), - $description_links + 'WooPayments' ); if ( WooPay_Utilities::is_store_country_available() ) { - $description = WC_Payments_Utils::esc_interpolated_html( - sprintf( - /* translators: %1$s: WooPayments, tosLink: Link to terms of service page, woopayMechantTosLink: Link to WooPay merchant terms, privacyLink: Link to privacy policy page */ - __( - 'Payments made simple — including WooPay, a new express checkout feature.

- By using %1$s you agree to be bound by our Terms of Service (including WooPay merchant terms) and acknowledge that you have read our Privacy Policy', - 'woocommerce-payments' - ), - 'WooPayments' + $description = sprintf( + /* translators: %s: WooPay, */ + __( + 'Payments made simple — including %s, a new express checkout feature.', + 'woocommerce-payments' ), - $description_links + 'WooPay' ); } @@ -4300,7 +4341,7 @@ private function upe_needs_redirection( $payment_methods ) { * @return void */ private function handle_afterpay_shipping_requirement( WC_Order $order, Create_And_Confirm_Intention $request ): void { - $check_if_usable = function( array $address ): bool { + $check_if_usable = function ( array $address ): bool { return $address['country'] && $address['state'] && $address['city'] && $address['postal_code'] && $address['line1']; }; diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index e4e07883fed..a3177576050 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -184,7 +184,7 @@ public function is_stripe_connected( bool $on_error = false ): bool { public function try_is_stripe_connected(): bool { $account = $this->get_cached_account_data(); if ( false === $account ) { - throw new Exception( __( 'Failed to detect connection status', 'woocommerce-payments' ) ); + throw new Exception( esc_html__( 'Failed to detect connection status', 'woocommerce-payments' ) ); } // The empty array indicates that account is not connected yet. @@ -315,7 +315,7 @@ public function get_account_status_data(): array { * * @return string Account statement descriptor. */ - public function get_statement_descriptor() : string { + public function get_statement_descriptor(): string { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['statement_descriptor'] ) ? $account['statement_descriptor'] : ''; } @@ -325,7 +325,7 @@ public function get_statement_descriptor() : string { * * @return string Account statement descriptor. */ - public function get_statement_descriptor_kanji() : string { + public function get_statement_descriptor_kanji(): string { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['statement_descriptor_kanji'] ) ? $account['statement_descriptor_kanji'] : ''; } @@ -335,7 +335,7 @@ public function get_statement_descriptor_kanji() : string { * * @return string Account statement descriptor. */ - public function get_statement_descriptor_kana() : string { + public function get_statement_descriptor_kana(): string { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['statement_descriptor_kana'] ) ? $account['statement_descriptor_kana'] : ''; } @@ -345,7 +345,7 @@ public function get_statement_descriptor_kana() : string { * * @return string Business profile name. */ - public function get_business_name() : string { + public function get_business_name(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['name'] ) ? $account['business_profile']['name'] : ''; } @@ -355,7 +355,7 @@ public function get_business_name() : string { * * @return string Business profile url. */ - public function get_business_url() : string { + public function get_business_url(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['url'] ) ? $account['business_profile']['url'] : ''; } @@ -365,7 +365,7 @@ public function get_business_url() : string { * * @return array Business profile support address. */ - public function get_business_support_address() : array { + public function get_business_support_address(): array { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['support_address'] ) ? $account['business_profile']['support_address'] : []; } @@ -375,7 +375,7 @@ public function get_business_support_address() : array { * * @return string Business profile support email. */ - public function get_business_support_email() : string { + public function get_business_support_email(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['support_email'] ) ? $account['business_profile']['support_email'] : ''; } @@ -385,7 +385,7 @@ public function get_business_support_email() : string { * * @return string Business profile support phone. */ - public function get_business_support_phone() : string { + public function get_business_support_phone(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['support_phone'] ) ? $account['business_profile']['support_phone'] : ''; } @@ -395,7 +395,7 @@ public function get_business_support_phone() : string { * * @return string branding logo. */ - public function get_branding_logo() : string { + public function get_branding_logo(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['logo'] ) ? $account['branding']['logo'] : ''; } @@ -405,7 +405,7 @@ public function get_branding_logo() : string { * * @return string branding icon. */ - public function get_branding_icon() : string { + public function get_branding_icon(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['icon'] ) ? $account['branding']['icon'] : ''; } @@ -415,7 +415,7 @@ public function get_branding_icon() : string { * * @return string branding primary color. */ - public function get_branding_primary_color() : string { + public function get_branding_primary_color(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['primary_color'] ) ? $account['branding']['primary_color'] : ''; } @@ -425,7 +425,7 @@ public function get_branding_primary_color() : string { * * @return string branding secondary color. */ - public function get_branding_secondary_color() : string { + public function get_branding_secondary_color(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['secondary_color'] ) ? $account['branding']['secondary_color'] : ''; } @@ -1847,7 +1847,7 @@ private function get_actioned_notes(): array { } // Fetch the last 10 actioned wcpay-promo admin notifications. - $add_like_clause = function( $where_clause ) { + $add_like_clause = function ( $where_clause ) { return $where_clause . " AND name like 'wcpay-promo-%'"; }; @@ -2023,7 +2023,7 @@ public function get_tracking_info( $force_refresh = false ): ?array { return $this->database_cache->get_or_add( Database_Cache::TRACKING_INFO_KEY, - function(): array { + function (): array { return $this->payments_api_client->get_tracking_info(); }, 'is_array', // We expect an array back from the cache. @@ -2053,7 +2053,12 @@ private function redirect_to_onboarding_flow_page( string $source ) { ); if ( ! $this->payments_api_client->is_server_connected() ) { - $this->payments_api_client->start_server_connection( $onboarding_url ); + try { + $this->payments_api_client->start_server_connection( $onboarding_url ); + } catch ( API_Exception $e ) { + // If we can't connect to the server, return, the error will be shown on the relevant page. + return; + } } else { $this->redirect_to( $onboarding_url ); } @@ -2094,6 +2099,16 @@ private function tracks_event( string $name, array $properties = [] ) { Logger::info( 'Tracks event: ' . $name . ' with data: ' . wp_json_encode( WC_Payments_Utils::redact_array( $properties, [ 'woo_country_code' ] ) ) ); } + /** + * Get the all-time total payment volume. + * + * @return int The all-time total payment volume, or null if not available. + */ + public function get_lifetime_total_payments_volume(): int { + $account = $this->get_cached_account_data(); + return (int) ! empty( $account ) && isset( $account['lifetime_total_payments_volume'] ) ? $account['lifetime_total_payments_volume'] : 0; + } + /** * Get user data to send to the onboarding flow. * diff --git a/includes/class-wc-payments-action-scheduler-service.php b/includes/class-wc-payments-action-scheduler-service.php index 1952d427a31..d66677834c6 100644 --- a/includes/class-wc-payments-action-scheduler-service.php +++ b/includes/class-wc-payments-action-scheduler-service.php @@ -38,7 +38,8 @@ class WC_Payments_Action_Scheduler_Service { * @param WC_Payments_Order_Service $order_service - Order Service. */ public function __construct( - WC_Payments_API_Client $payments_api_client, WC_Payments_Order_Service $order_service + WC_Payments_API_Client $payments_api_client, + WC_Payments_Order_Service $order_service ) { $this->payments_api_client = $payments_api_client; $this->order_service = $order_service; diff --git a/includes/class-wc-payments-apple-pay-registration.php b/includes/class-wc-payments-apple-pay-registration.php index 276a6e65c62..cc45ef29c79 100644 --- a/includes/class-wc-payments-apple-pay-registration.php +++ b/includes/class-wc-payments-apple-pay-registration.php @@ -418,7 +418,7 @@ public function display_error_notice() { $learn_more_text = WC_Payments_Utils::esc_interpolated_html( __( '
Learn more.', 'woocommerce-payments' ), [ - 'a' => '', + 'a' => '', ] ); diff --git a/includes/class-wc-payments-blocks-payment-method.php b/includes/class-wc-payments-blocks-payment-method.php index 8d5fd8af753..947cc94a5c9 100644 --- a/includes/class-wc-payments-blocks-payment-method.php +++ b/includes/class-wc-payments-blocks-payment-method.php @@ -74,6 +74,21 @@ public function get_payment_method_script_handles() { WC_Payments::register_script_with_dependencies( 'WCPAY_BLOCKS_CHECKOUT', 'dist/blocks-checkout', [ 'stripe' ] ); wp_set_script_translations( 'WCPAY_BLOCKS_CHECKOUT', 'woocommerce-payments' ); + if ( WC()->cart ) { + wp_add_inline_script( + 'WCPAY_BLOCKS_CHECKOUT', + 'var wcBlocksCheckoutData = ' . wp_json_encode( + [ + 'amount' => WC()->cart->get_total( '' ), + 'currency' => get_woocommerce_currency(), + 'storeCountry' => WC()->countries->get_base_country(), + 'billingCountry' => WC()->customer->get_billing_country(), + ] + ) . ';', + 'before' + ); + } + Fraud_Prevention_Service::maybe_append_fraud_prevention_token(); return [ 'WCPAY_BLOCKS_CHECKOUT' ]; diff --git a/includes/class-wc-payments-captured-event-note.php b/includes/class-wc-payments-captured-event-note.php index 40209e86927..f5b38ada258 100644 --- a/includes/class-wc-payments-captured-event-note.php +++ b/includes/class-wc-payments-captured-event-note.php @@ -390,7 +390,6 @@ private function format_fx( self::format_exchange_rate( $exchange_rate, $to_currency ), WC_Payments_Utils::format_explicit_currency( $to_display_amount, $to_currency, false ) ); - } /** @@ -410,7 +409,7 @@ private function format_exchange_rate( float $rate, string $currency ): string { [ 'decimals' => $num_decimals ] ); - $func_remove_ending_zeros = function( $str ) { + $func_remove_ending_zeros = function ( $str ) { return rtrim( $str, '0' ); }; diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 40f973722f3..5cb398bcdd3 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -221,6 +221,10 @@ public function get_payment_fields_js_config() { $payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT ); $payment_fields['upeBnplProductPageAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT ); $payment_fields['upeBnplProductPageAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT ); + $payment_fields['upeBnplClassicCartAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT ); + $payment_fields['upeBnplClassicCartAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT ); + $payment_fields['upeBnplCartBlockAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT ); + $payment_fields['upeBnplCartBlockAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT ); $payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); $payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT ); $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart(); @@ -272,6 +276,12 @@ public function get_payment_fields_js_config() { } } + // Get the store base country. + $payment_fields['storeCountry'] = WC()->countries->get_base_country(); + + // Get the WooCommerce Store API endpoint. + $payment_fields['storeApiURL'] = get_rest_url( null, 'wc/store' ); + /** * Allows filtering for the payment fields. * @@ -314,7 +324,7 @@ public function get_enabled_payment_method_config() { $payment_method->get_testing_instructions(), [ 'strong' => '', - 'a' => '', + 'a' => '', ] ); $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); @@ -354,7 +364,7 @@ public function payment_fields() { wp_enqueue_script( 'wcpay-upe-checkout' ); add_action( 'wp_footer', - function() use ( $payment_fields ) { + function () use ( $payment_fields ) { wp_localize_script( 'wcpay-upe-checkout', 'wcpay_upe_config', $payment_fields ); } ); @@ -383,12 +393,13 @@ function() use ( $payment_fields ) { gateway->get_payment_method()->get_testing_instructions(); if ( false !== $testing_instructions ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo WC_Payments_Utils::esc_interpolated_html( /* translators: link to Stripe testing page */ $testing_instructions, [ 'strong' => '', - 'a' => '', + 'a' => '', ] ); } @@ -448,5 +459,4 @@ public function set_gateway( $payment_method_id ) { $this->gateway = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); } } - } diff --git a/includes/class-wc-payments-db.php b/includes/class-wc-payments-db.php index 95692fab841..dc9cc0a458c 100644 --- a/includes/class-wc-payments-db.php +++ b/includes/class-wc-payments-db.php @@ -50,7 +50,7 @@ public function orders_with_charge_id_from_charge_ids( array $charge_ids ): arra ); return array_map( - function ( WC_Order $order ) : array { + function ( WC_Order $order ): array { return [ 'order' => $order, 'charge_id' => $order->get_meta( self::META_KEY_CHARGE_ID ), @@ -58,7 +58,6 @@ function ( WC_Order $order ) : array { }, $orders ); - } /** diff --git a/includes/class-wc-payments-dependency-service.php b/includes/class-wc-payments-dependency-service.php index 3934be7ebad..a976febb405 100644 --- a/includes/class-wc-payments-dependency-service.php +++ b/includes/class-wc-payments-dependency-service.php @@ -45,7 +45,6 @@ public function has_valid_dependencies() { } return empty( $this->get_invalid_dependencies( true ) ); - } /** @@ -107,7 +106,6 @@ public function get_invalid_dependencies( bool $check_account_connection = false } return $invalid_dependencies; - } /** diff --git a/includes/class-wc-payments-file-service.php b/includes/class-wc-payments-file-service.php index 784d39d0532..f8eea316d9b 100644 --- a/includes/class-wc-payments-file-service.php +++ b/includes/class-wc-payments-file-service.php @@ -29,8 +29,7 @@ class WC_Payments_File_Service { * * @return bool */ - public function is_file_public( string $purpose ) : bool { + public function is_file_public( string $purpose ): bool { return in_array( $purpose, static::FILE_PURPOSE_PUBLIC, true ); } - } diff --git a/includes/class-wc-payments-fraud-service.php b/includes/class-wc-payments-fraud-service.php index 75071acce6b..2a500e5e34a 100644 --- a/includes/class-wc-payments-fraud-service.php +++ b/includes/class-wc-payments-fraud-service.php @@ -256,7 +256,7 @@ function () { // This is OK to do since we are not accepting data entries with HTML. return WC_Payments_Utils::array_map_recursive( $fraud_services, - function( $value ) { + function ( $value ) { // Only apply `sanitize_text_field()` to string values since this function will cast to string. if ( is_string( $value ) ) { return sanitize_text_field( $value ); diff --git a/includes/class-wc-payments-incentives-service.php b/includes/class-wc-payments-incentives-service.php index 77fc841e523..692f744669e 100644 --- a/includes/class-wc-payments-incentives-service.php +++ b/includes/class-wc-payments-incentives-service.php @@ -184,7 +184,7 @@ public function fetch_connect_incentive_details(): ?array { if ( ! empty( $results ) ) { $incentive = array_filter( $results, - function( array $incentive ) { + function ( array $incentive ) { return isset( $incentive['type'] ) && 'connect_page' === $incentive['type']; } )[0] ?? []; diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 2fd28026802..eb7da0b288c 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -259,7 +259,7 @@ public static function is_test_mode_enabled(): bool { * * @return string The source or empty string if the source is unsupported. */ - public static function get_source( string $referer, array $get_params ) : string { + public static function get_source( string $referer, array $get_params ): string { $wcpay_connect_param = sanitize_text_field( wp_unslash( $get_params['wcpay-connect'] ) ); if ( 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param ) { return self::SOURCE_WCADMIN_PAYMENT_TASK; diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php index f5bd1c2458f..b3e45084522 100644 --- a/includes/class-wc-payments-order-service.php +++ b/includes/class-wc-payments-order-service.php @@ -476,7 +476,7 @@ function ( array $event ) { * * @throws Order_Not_Found_Exception */ - public function get_intent_id_for_order( $order ) : string { + public function get_intent_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::INTENT_ID_META_KEY, true ); } @@ -510,7 +510,7 @@ public function set_intent_id_for_order( $order, $intent_id ) { * * @throws Order_Not_Found_Exception */ - public function get_payment_method_id_for_order( $order ) : string { + public function get_payment_method_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::PAYMENT_METHOD_ID_META_KEY, true ); } @@ -575,7 +575,7 @@ public function set_payment_transaction_id_for_order( $order, $payment_transacti * * @throws Order_Not_Found_Exception */ - public function get_charge_id_for_order( $order ) : string { + public function get_charge_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::CHARGE_ID_META_KEY, true ); } @@ -603,7 +603,7 @@ public function set_intention_status_for_order( $order, $intention_status ) { * * @throws Order_Not_Found_Exception */ - public function get_intention_status_for_order( $order ) : string { + public function get_intention_status_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::INTENTION_STATUS_META_KEY, true ); } @@ -617,7 +617,7 @@ public function get_intention_status_for_order( $order ) : string { * * @throws Order_Not_Found_Exception */ - public function has_open_authorization( $order ) : bool { + public function has_open_authorization( $order ): bool { $order = $this->get_order( $order ); return Intent_Status::REQUIRES_CAPTURE === $order->get_meta( self::INTENTION_STATUS_META_KEY, true ); } @@ -646,7 +646,7 @@ public function set_customer_id_for_order( $order, $customer_id ) { * * @throws Order_Not_Found_Exception */ - public function get_customer_id_for_order( $order ) : string { + public function get_customer_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::CUSTOMER_ID_META_KEY, true ); } @@ -674,7 +674,7 @@ public function set_wcpay_intent_currency_for_order( $order, $wcpay_intent_curre * * @throws Order_Not_Found_Exception */ - public function get_wcpay_intent_currency_for_order( $order ) : string { + public function get_wcpay_intent_currency_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_INTENT_CURRENCY_META_KEY, true ); } @@ -716,7 +716,7 @@ public function set_wcpay_refund_transaction_id_for_order( WC_Order_Refund $orde * * @throws Order_Not_Found_Exception */ - public function get_wcpay_refund_id_for_order( $order ) : string { + public function get_wcpay_refund_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_REFUND_ID_META_KEY, true ); } @@ -744,7 +744,7 @@ public function set_wcpay_refund_status_for_order( $order, $wcpay_refund_status * * @throws Order_Not_Found_Exception */ - public function get_wcpay_refund_status_for_order( $order ) : string { + public function get_wcpay_refund_status_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_REFUND_STATUS_META_KEY, true ); } @@ -772,7 +772,7 @@ public function set_fraud_outcome_status_for_order( $order, $fraud_outcome_statu * * @throws Order_Not_Found_Exception */ - public function get_fraud_outcome_status_for_order( $order ) : string { + public function get_fraud_outcome_status_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_FRAUD_OUTCOME_STATUS_META_KEY, true ); } @@ -800,7 +800,7 @@ public function set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type * * @throws Order_Not_Found_Exception */ - public function get_fraud_meta_box_type_for_order( $order ) : string { + public function get_fraud_meta_box_type_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_FRAUD_META_BOX_TYPE_META_KEY, true ); } @@ -1138,7 +1138,6 @@ public function attach_transaction_fee_to_order( $order, $charge ) { // Log the error and don't block checkout. Logger::log( 'Error saving transaction fee into metadata for the order ' . $order->get_id() . ': ' . $e->getMessage() ); } - } /** @@ -1266,7 +1265,7 @@ public function create_refund_for_order( WC_Order $order, float $amount, string ); if ( is_wp_error( $refund ) ) { - throw new Exception( $refund->get_error_message() ); + throw new Exception( esc_html( $refund->get_error_message() ) ); } return $refund; @@ -1495,7 +1494,6 @@ private function generate_capture_expired_note( $intent_id, $charge_id ) { ), WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); - } /** @@ -1917,7 +1915,7 @@ private function get_order( $order ) { $order = $this->is_order_type_object( $order ) ? $order : wc_get_order( $order ); if ( ! $this->is_order_type_object( $order ) ) { throw new Order_Not_Found_Exception( - __( 'The requested order was not found.', 'woocommerce-payments' ), + esc_html__( 'The requested order was not found.', 'woocommerce-payments' ), 'order_not_found' ); } diff --git a/includes/class-wc-payments-payment-method-messaging-element.php b/includes/class-wc-payments-payment-method-messaging-element.php index 7148e5d8c52..d27409d1be0 100644 --- a/includes/class-wc-payments-payment-method-messaging-element.php +++ b/includes/class-wc-payments-payment-method-messaging-element.php @@ -42,27 +42,38 @@ public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WC /** * Initializes the payment method messaging element. * - * @return string The HTML markup for the payment method message container. + * @return string|void The HTML markup for the payment method message container. */ - public function init(): string { + public function init() { + + $is_cart_block = WC_Payments_Utils::is_cart_block(); + + if ( ! is_product() && ! is_cart() && ! $is_cart_block ) { + return; + } + global $product; - $currency_code = get_woocommerce_currency(); - $store_country = WC()->countries->get_base_country(); - $billing_country = WC()->customer->get_billing_country(); + $currency_code = get_woocommerce_currency(); + $store_country = WC()->countries->get_base_country(); + $billing_country = WC()->customer->get_billing_country(); + $cart_total = WC()->cart->total; + $product_variations = []; - $product_variations = [ - 'base_product' => [ - 'amount' => WC_Payments_Utils::prepare_amount( $product->get_price(), $currency_code ), - 'currency' => $currency_code, - ], - ]; - foreach ( $product->get_children() as $variation_id ) { - $variation = wc_get_product( $variation_id ); - if ( $variation ) { - $product_variations[ $variation_id ] = [ - 'amount' => WC_Payments_Utils::prepare_amount( $variation->get_price(), $currency_code ), + if ( $product ) { + $product_variations = [ + 'base_product' => [ + 'amount' => WC_Payments_Utils::prepare_amount( $product->get_price(), $currency_code ), 'currency' => $currency_code, - ]; + ], + ]; + foreach ( $product->get_children() as $variation_id ) { + $variation = wc_get_product( $variation_id ); + if ( $variation ) { + $product_variations[ $variation_id ] = [ + 'amount' => WC_Payments_Utils::prepare_amount( $variation->get_price(), $currency_code ), + 'currency' => $currency_code, + ]; + } } } @@ -94,6 +105,12 @@ public function init(): string { 'accountId' => $this->account->get_stripe_account_id(), 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), 'paymentMethods' => array_values( $bnpl_payment_methods ), + 'currencyCode' => $currency_code, + 'isCart' => is_cart(), + 'isCartBlock' => $is_cart_block, + 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), + 'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ), + 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), ] ); @@ -107,6 +124,8 @@ public function init(): string { 'before' ); - return '

'; + if ( ! $is_cart_block ) { + return '
'; + } } } diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 221240075bc..d4f8c568a87 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -251,12 +251,13 @@ public function get_product_price( $product, ?bool $is_deposit = null, int $depo } if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) { + $error_message = sprintf( + // Translators: %d is the numeric ID of the product without a price. + __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ), + (int) $product->get_id() + ); throw new Invalid_Price_Exception( - sprintf( - // Translators: %d is the numeric ID of the product without a price. - __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ), - $product->get_id() - ) + esc_html( $error_message ) ); } @@ -861,15 +862,15 @@ public function ajax_get_shipping_options() { $shipping_address = filter_input_array( INPUT_POST, [ - 'country' => FILTER_SANITIZE_STRING, - 'state' => FILTER_SANITIZE_STRING, - 'postcode' => FILTER_SANITIZE_STRING, - 'city' => FILTER_SANITIZE_STRING, - 'address_1' => FILTER_SANITIZE_STRING, - 'address_2' => FILTER_SANITIZE_STRING, + 'country' => FILTER_SANITIZE_SPECIAL_CHARS, + 'state' => FILTER_SANITIZE_SPECIAL_CHARS, + 'postcode' => FILTER_SANITIZE_SPECIAL_CHARS, + 'city' => FILTER_SANITIZE_SPECIAL_CHARS, + 'address_1' => FILTER_SANITIZE_SPECIAL_CHARS, + 'address_2' => FILTER_SANITIZE_SPECIAL_CHARS, ] ); - $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_STRING ] ); + $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); $data = $this->get_shipping_options( $shipping_address, $should_show_itemized_view ); @@ -892,7 +893,7 @@ public function get_shipping_options( $shipping_address, $itemized_display_items $data = []; // Remember current shipping method before resetting. - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); $this->calculate_shipping( apply_filters( 'wcpay_payment_request_shipping_posted_values', $shipping_address ) ); $packages = WC()->shipping->get_packages(); @@ -942,6 +943,8 @@ public function get_shipping_options( $shipping_address, $itemized_display_items WC()->cart->calculate_totals(); + $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods ); + $data += $this->express_checkout_helper->build_display_items( $itemized_display_items ); $data['result'] = 'success'; } catch ( Exception $e ) { @@ -967,7 +970,7 @@ public function ajax_update_shipping_method() { WC()->cart->calculate_totals(); - $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_STRING ] ); + $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); $data = []; @@ -1506,4 +1509,40 @@ private function get_taxes_like_cart( $product, $price ) { // Normally there should be a single tax, but `calc_tax` returns an array, let's use it. return WC_Tax::calc_tax( $price, $rates, false ); } + + /** + * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated + * during the Payment Request get_shipping_options flow. + * + * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping + * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's + * only concerned about handling the initial purchase. + * + * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore + * the previously chosen shipping methods for each recurring cart. + * + * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet. + * + * @param array $previous_chosen_methods The previously chosen shipping methods. + */ + private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { + if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) { + return; + } + + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); + + foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { + foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) { + $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index ); + + // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it. + if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) { + $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ]; + } + } + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } } diff --git a/includes/class-wc-payments-session-service.php b/includes/class-wc-payments-session-service.php index 6410c684073..f4aadd68a4a 100644 --- a/includes/class-wc-payments-session-service.php +++ b/includes/class-wc-payments-session-service.php @@ -164,7 +164,7 @@ private function generate_store_id(): string { $char_length = strlen( $include_chars ); $random_string = ''; - for ( $i = 0; $i < $length; $i ++ ) { + for ( $i = 0; $i < $length; $i++ ) { $random_string .= $include_chars [ wp_rand( 0, $char_length - 1 ) ]; } diff --git a/includes/class-wc-payments-status.php b/includes/class-wc-payments-status.php index 6f3caf46c14..da480abe413 100644 --- a/includes/class-wc-payments-status.php +++ b/includes/class-wc-payments-status.php @@ -83,7 +83,8 @@ public function debug_tools( $tools ) { /** * Renders WCPay information on the status page. */ - public function render_status_report_section() { ?> + public function render_status_report_section() { + ?> @@ -98,7 +99,7 @@ public function render_status_report_section() { ?> @@ -108,7 +109,7 @@ public function render_status_report_section() { ?> @@ -116,12 +117,12 @@ public function render_status_report_section() { ?> http->is_connected() ) : ?> - + - + ?> - + - + - + @@ -150,7 +151,7 @@ public function render_status_report_section() { ?> @@ -158,7 +159,7 @@ public function render_status_report_section() { ?> - + - + - + - + gateway->get_option( 'current_protection_level' ) === 'advanced' ) : ?> - + - + - + - + - + - + diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php index a2ae107f9bb..37fa25ced0e 100644 --- a/includes/class-wc-payments-token-service.php +++ b/includes/class-wc-payments-token-service.php @@ -86,7 +86,7 @@ public function add_token_to_user( $payment_method, $user ) { $token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID ); $token->set_expiry_month( $payment_method[ Payment_Method::CARD ]['exp_month'] ); $token->set_expiry_year( $payment_method[ Payment_Method::CARD ]['exp_year'] ); - $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD ]['brand'] ) ); + $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD ]['display_brand'] ?? $payment_method[ Payment_Method::CARD ]['networks']['preferred'] ?? $payment_method[ Payment_Method::CARD ]['brand'] ) ); $token->set_last4( $payment_method[ Payment_Method::CARD ]['last4'] ); } diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 8f56863e34f..62efd5ecfc9 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -219,7 +219,7 @@ public static function zero_decimal_currencies(): array { /** * List of countries enabled for Stripe platform account. See also this URL: - * https://woo.com/document/woopayments/compatibility/countries/#supported-countries + * https://woocommerce.com/document/woopayments/compatibility/countries/#supported-countries * * @return string[] */ @@ -263,6 +263,7 @@ public static function supported_countries(): array { Country_Code::SLOVAKIA => __( 'Slovakia', 'woocommerce-payments' ), Country_Code::SINGAPORE => __( 'Singapore', 'woocommerce-payments' ), Country_Code::UNITED_STATES => __( 'United States (US)', 'woocommerce-payments' ), + Country_Code::PUERTO_RICO => __( 'Puerto Rico', 'woocommerce-payments' ), ]; } @@ -337,28 +338,41 @@ public static function map_search_orders_to_charge_ids( $search ) { } /** - * Extract the billing details from the WC order + * Extract the billing details from the WC order. + * It only returns the fields that are present in the billing section of the checkout. * * @param WC_Order $order Order to extract the billing details from. * * @return array */ public static function get_billing_details_from_order( $order ) { - $billing_details = [ - 'address' => [ - 'city' => $order->get_billing_city(), - 'country' => $order->get_billing_country(), - 'line1' => $order->get_billing_address_1(), - 'line2' => $order->get_billing_address_2(), - 'postal_code' => $order->get_billing_postcode(), - 'state' => $order->get_billing_state(), - ], - 'email' => $order->get_billing_email(), - 'name' => trim( $order->get_formatted_billing_full_name() ), - 'phone' => $order->get_billing_phone(), + $billing_fields = array_keys( WC()->checkout()->get_checkout_fields( 'billing' ) ); + $address_field_to_key = [ + 'billing_city' => 'city', + 'billing_country' => 'country', + 'billing_address_1' => 'line1', + 'billing_address_2' => 'line2', + 'billing_postcode' => 'postal_code', + 'billing_state' => 'state', ]; + $field_to_key = [ + 'billing_email' => 'email', + 'billing_phone' => 'phone', + ]; + $billing_details = [ 'address' => [] ]; + foreach ( $billing_fields as $field ) { + if ( isset( $address_field_to_key[ $field ] ) ) { + $billing_details['address'][ $address_field_to_key[ $field ] ] = $order->{"get_{$field}"}(); + } elseif ( isset( $field_to_key[ $field ] ) ) { + $billing_details[ $field_to_key[ $field ] ] = $order->{"get_{$field}"}(); + } + } + + if ( in_array( 'billing_first_name', $billing_fields, true ) && in_array( 'billing_last_name', $billing_fields, true ) ) { + $billing_details['name'] = trim( $order->get_formatted_billing_full_name() ); + } - return array_filter( $billing_details ); + return $billing_details; } /** @@ -592,11 +606,20 @@ public static function get_filtered_error_message( Exception $e ) { * * @return int */ - public static function get_filtered_error_status_code( Exception $e ) : int { + public static function get_filtered_error_status_code( Exception $e ): int { + $status_code = null; if ( $e instanceof API_Exception ) { - return $e->get_http_code() ?? 400; + $status_code = $e->get_http_code(); + } + + // Hosting companies might use the 402 status code to return a custom error page. + // When 402 is returned by Stripe, let's return 400 instead. + // The frontend doesn't make use of the status code. + if ( 402 === $status_code ) { + $status_code = 400; } - return 400; + + return $status_code ?? 400; } /** @@ -1074,4 +1097,16 @@ public static function convert_to_server_locale( string $locale ): string { public static function is_cart_page(): bool { return is_cart() || has_block( 'woocommerce/cart' ); } + + /** + * 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. + * + * @return bool + * + * @psalm-suppress UndefinedFunction + */ + public static function is_cart_block(): bool { + return has_block( 'woocommerce/cart' ) || ( wp_is_block_theme() && is_cart() ); + } } diff --git a/includes/class-wc-payments-webhook-processing-service.php b/includes/class-wc-payments-webhook-processing-service.php index 39c9d70ba78..e9806dbc395 100644 --- a/includes/class-wc-payments-webhook-processing-service.php +++ b/includes/class-wc-payments-webhook-processing-service.php @@ -142,7 +142,7 @@ public function process( array $event_body ) { if ( $this->is_webhook_mode_mismatch( $event_body ) ) { return; - }; + } try { do_action( 'woocommerce_payments_before_webhook_delivery', $event_type, $event_body ); @@ -757,7 +757,7 @@ private function get_order_from_event_body_intent_id( $event_body ) { * * @return string The failure message. */ - private function get_failure_message_from_error( $error ):string { + private function get_failure_message_from_error( $error ): string { $code = $error['code'] ?? ''; $decline_code = $error['decline_code'] ?? ''; $message = $error['message'] ?? ''; diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 9402cd5c695..22c21a3c4f4 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -409,6 +409,7 @@ public static function init() { include_once __DIR__ . '/exceptions/class-add-payment-method-exception.php'; include_once __DIR__ . '/exceptions/class-amount-too-large-exception.php'; include_once __DIR__ . '/exceptions/class-amount-too-small-exception.php'; + include_once __DIR__ . '/exceptions/class-cannot-combine-currencies-exception.php'; include_once __DIR__ . '/exceptions/class-intent-authentication-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-payment-method-exception.php'; include_once __DIR__ . '/exceptions/class-process-payment-exception.php'; @@ -572,6 +573,9 @@ public static function init() { ); if ( [] !== $enabled_bnpl_payment_methods ) { add_action( 'woocommerce_single_product_summary', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 10 ); + add_action( 'woocommerce_proceed_to_checkout', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 10 ); + add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ] ); + add_action( 'wc_ajax_wcpay_get_cart_total', [ __CLASS__, 'ajax_get_cart_total' ] ); } add_filter( 'woocommerce_payment_gateways', [ __CLASS__, 'register_gateway' ] ); @@ -631,6 +635,10 @@ public static function init() { $admin_settings = new WC_Payments_Admin_Settings( self::get_gateway() ); $admin_settings->init_hooks(); + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-payments-bnpl-announcement.php'; + $bnpl_announcement = new WC_Payments_Bnpl_Announcement( self::get_gateway(), self::get_account_service(), time() ); + $bnpl_announcement->init_hooks(); + // Use tracks loader only in admin screens because it relies on WC_Tracks loaded by WC_Admin. include_once WCPAY_ABSPATH . 'includes/admin/tracks/tracks-loader.php'; @@ -1062,7 +1070,6 @@ public static function init_rest_api() { include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-woopay-session-controller.php'; $woopay_session_controller = new WC_REST_WooPay_Session_Controller(); $woopay_session_controller->register_routes(); - } /** @@ -1385,7 +1392,7 @@ function wcpay_show_old_woocommerce_for_norway_notice() {

the plugins page.', 'woocommerce-payments' ), @@ -1603,6 +1610,28 @@ public static function ajax_get_woopay_signature() { ); } + /** + * Get cart total. + */ + public static function ajax_get_cart_total() { + check_ajax_referer( 'wcpay-get-cart-total', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + WC()->cart->calculate_totals(); + + $cart_total = WC()->cart->total; + $currency_code = get_woocommerce_currency(); + + wp_send_json( [ 'total' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ) ] ); + } + /** * Adds custom email field. */ @@ -1664,6 +1693,11 @@ public static function enqueue_cart_scripts() { self::register_script_with_dependencies( 'WCPAY_CART', 'dist/cart' ); wp_enqueue_script( 'WCPAY_CART' ); + + if ( WC_Payments_Utils::is_cart_block() ) { + self::register_script_with_dependencies( 'WCPAY_CART_BLOCK', 'dist/cart-block', [ 'wc-cart-block-frontend' ] ); + wp_enqueue_script( 'WCPAY_CART_BLOCK' ); + } } /** @@ -1836,7 +1870,7 @@ public static function wcpay_show_old_woocommerce_for_hungary_sweden_and_czech_r

maybe_record_event( sanitize_text_field( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data, $is_legacy_event ); + $this->maybe_record_event( sanitize_text_field( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data ); wp_send_json_success(); } @@ -132,13 +122,11 @@ public function ajax_tracks_id() { * * @param string $event name of the event. * @param array $data array of event properties. - * @param boolean $is_legacy indicate whether this is a legacy event. */ - public function maybe_record_event( $event, $data = [], $is_legacy = true ) { + public function maybe_record_event( $event, $data = [] ) { // Top level events should not be namespaced. if ( '_aliasUser' !== $event ) { - $prefix = $is_legacy ? self::$legacy_user_prefix : self::$user_prefix; - $event = $prefix . '_' . $event; + $event = self::$user_prefix . '_' . $event; } return $this->tracks_record_event( $event, $data ); diff --git a/includes/compat/blocks/class-blocks-data-extractor.php b/includes/compat/blocks/class-blocks-data-extractor.php index 923469622e7..c24e90649ce 100644 --- a/includes/compat/blocks/class-blocks-data-extractor.php +++ b/includes/compat/blocks/class-blocks-data-extractor.php @@ -141,7 +141,7 @@ public function get_data() { * * @return array */ - public function get_checkout_schema_namespaces() : array { + public function get_checkout_schema_namespaces(): array { $namespaces = []; if ( diff --git a/includes/constants/class-base-constant.php b/includes/constants/class-base-constant.php index 6c2a177dbe1..be1a5770922 100644 --- a/includes/constants/class-base-constant.php +++ b/includes/constants/class-base-constant.php @@ -42,10 +42,8 @@ abstract class Base_Constant implements \JsonSerializable { private function __construct( string $value ) { if ( $value instanceof static ) { $value = $value->get_value(); - } else { - if ( ! defined( static::class . "::$value" ) ) { + } elseif ( ! defined( static::class . "::$value" ) ) { throw new \InvalidArgumentException( "Constant with name '$value' does not exist." ); - } } $this->value = $value; diff --git a/includes/constants/class-country-code.php b/includes/constants/class-country-code.php index 790059d65ef..1fec03e529e 100644 --- a/includes/constants/class-country-code.php +++ b/includes/constants/class-country-code.php @@ -172,6 +172,7 @@ class Country_Code extends Base_Constant { const PHILIPPINES = 'PH'; const POLAND = 'PL'; const PORTUGAL = 'PT'; + const PUERTO_RICO = 'PR'; const QATAR = 'QA'; const ROMANIA = 'RO'; const RUSSIA = 'RU'; diff --git a/includes/core/CONTRIBUTING.md b/includes/core/CONTRIBUTING.md index 4e7c896270c..a36e462c863 100644 --- a/includes/core/CONTRIBUTING.md +++ b/includes/core/CONTRIBUTING.md @@ -12,7 +12,7 @@ There are a few possible paths when it comes to services: 1. __Create a facade for an existing service:__ Create a new service class within `core/service`, which simply facades the [existing service](service/customer-service.md). Doing so will allow us to modify the facade in the future, keeping existing methods with the same parameters as existing ones. This is what was done with the [customer service](service/customer-service.md), and is the recommended way if a certain feature requires access to an existing service quickly. -2. __Move an existing service to the core directory:__ This should be done with consideration how the service could change in the future, and whether it is core to the gateway. If it more suitable to an extension (ex. [Multi-Currency](https://woo.com/document/woopayments/currencies/multi-currency-setup/)), or a consumer (ex. [WooPay](https://woo.com/documentation/products/woopay/)), it likely needs to be somewhere else. +2. __Move an existing service to the core directory:__ This should be done with consideration how the service could change in the future, and whether it is core to the gateway. If it more suitable to an extension (ex. [Multi-Currency](https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/)), or a consumer (ex. [WooPay](https://woocommerce.com/documentation/products/woopay/)), it likely needs to be somewhere else. 3. When __creating a new service__, similarly to moving existing ones here, please consider whether the service belongs to core. If it does, do it with care, as services should be reliable and resilient. 🔗 Further information about services in core is available [within the services directory](services/README.md). diff --git a/includes/core/class-mode.php b/includes/core/class-mode.php index 721b8ea689d..8068bcf0754 100644 --- a/includes/core/class-mode.php +++ b/includes/core/class-mode.php @@ -68,7 +68,7 @@ private function maybe_init() { /** * Allows WooPayments to enter dev (aka sandbox) mode. * - * @see https://woo.com/document/woopayments/testing-and-troubleshooting/sandbox-mode/ + * @see https://woocommerce.com/document/woopayments/testing-and-troubleshooting/sandbox-mode/ * @param bool $dev_mode The pre-determined dev mode. */ $this->dev_mode = (bool) apply_filters( 'wcpay_dev_mode', $dev_mode ); @@ -82,7 +82,7 @@ private function maybe_init() { /** * Allows WooPayments to enter test mode. * - * @see https://woo.com/document/woopayments/testing-and-troubleshooting/testing/#enabling-test-mode + * @see https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#enabling-test-mode * @param bool $test_mode The pre-determined test mode. */ $this->test_mode = (bool) apply_filters( 'wcpay_test_mode', $test_mode ); @@ -94,7 +94,7 @@ private function maybe_init() { * @throws Exception In case the class has not been initialized yet. * @return bool */ - public function is_live() : bool { + public function is_live(): bool { $this->maybe_init(); return ! $this->test_mode && ! $this->dev_mode; } @@ -105,7 +105,7 @@ public function is_live() : bool { * @throws Exception In case the class has not been initialized yet. * @return bool */ - public function is_test() : bool { + public function is_test(): bool { $this->maybe_init(); return $this->test_mode; @@ -117,7 +117,7 @@ public function is_test() : bool { * @throws Exception In case the class has not been initialized yet. * @return bool */ - public function is_dev() : bool { + public function is_dev(): bool { $this->maybe_init(); return $this->dev_mode; } @@ -157,7 +157,7 @@ public function dev() { * * @return bool Whether `WCPAY_DEV_MODE` is defined and true. */ - protected function is_wcpay_dev_mode_defined() : bool { + protected function is_wcpay_dev_mode_defined(): bool { return( defined( 'WCPAY_DEV_MODE' ) && WCPAY_DEV_MODE diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index c2e83e73041..ddf1f042e8d 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -273,10 +273,12 @@ final public function get_params() { if ( ! empty( $missing_params ) ) { throw new Invalid_Request_Parameter_Exception( - sprintf( - 'Trying to access the parameters of a request which is not (fully) initialized yet. Missing parameter(s) for %s: %s', - get_class( $this ), - implode( ', ', $missing_params ) + esc_html( + sprintf( + 'Trying to access the parameters of a request which is not (fully) initialized yet. Missing parameter(s) for %s: %s', + get_class( $this ), + implode( ', ', $missing_params ) + ) ), 'wcpay_core_invalid_request_parameter_missing_parameters' ); @@ -305,9 +307,11 @@ final public function get_param( $key ) { return $this->params[ $key ]; } throw new Invalid_Request_Parameter_Exception( - sprintf( - 'The passed key %s does not exist in Request class', - $key + esc_html( + sprintf( + 'The passed key %s does not exist in Request class', + $key + ) ), 'wcpay_core_invalid_request_parameter_uninitialized_param' ); @@ -457,7 +461,7 @@ final public static function extend( Request $base_request ) { if ( ! $base_request->protected_mode ) { throw new Extend_Request_Exception( - get_class( $base_request ) . ' can only be extended within its ->apply_filters() method.', + esc_html( get_class( $base_request ) . ' can only be extended within its ->apply_filters() method.' ), 'wcpay_core_extend_class_incorrectly' ); } @@ -538,10 +542,12 @@ final public function apply_filters( $hook, ...$args ) { */ private function throw_immutable_exception( string $param ) { throw new Immutable_Parameter_Exception( - sprintf( - 'The value of %s::%s is immutable and cannot be changed.', - get_class( $this ), - $param + esc_html( + sprintf( + 'The value of %s::%s is immutable and cannot be changed.', + get_class( $this ), + $param + ) ), 'wcpay_core_immutable_parameter_changed' ); @@ -608,7 +614,7 @@ public static function traverse_class_constants( string $constant_name, bool $un * @return array The difference between the two arrays. */ private function array_diff( $array1, $array2 ) { - $arr_to_json = function( $item ) { + $arr_to_json = function ( $item ) { return is_array( $item ) ? wp_json_encode( $item ) : $item; }; @@ -629,7 +635,7 @@ private function array_diff( $array1, $array2 ) { protected function validate_stripe_id( $id, $prefixes = null ) { if ( empty( $id ) ) { throw new Invalid_Request_Parameter_Exception( - __( 'Empty parameter is not allowed', 'woocommerce-payments' ), + esc_html__( 'Empty parameter is not allowed', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_stripe_id' ); } @@ -657,10 +663,12 @@ protected function validate_stripe_id( $id, $prefixes = null ) { } throw new Invalid_Request_Parameter_Exception( - sprintf( + esc_html( + sprintf( // Translators: %s is a Stripe ID. - __( '%s is not a valid Stripe identifier', 'woocommerce-payments' ), - $id + __( '%s is not a valid Stripe identifier', 'woocommerce-payments' ), + $id + ) ), 'wcpay_core_invalid_request_parameter_stripe_id' ); @@ -680,11 +688,13 @@ protected function validate_is_larger_than( float $value_to_validate, float $val } throw new Invalid_Request_Parameter_Exception( - sprintf( + esc_html( + sprintf( /* translators: %1$s and %2$s are both numbers */ - __( 'Invalid number passed. Number %1$s needs to be larger than %2$s', 'woocommerce-payments' ), - $value_to_validate, - $value_to_compare + __( 'Invalid number passed. Number %1$s needs to be larger than %2$s', 'woocommerce-payments' ), + $value_to_validate, + $value_to_compare + ) ), 'wcpay_core_invalid_request_parameter_order' ); @@ -702,10 +712,12 @@ public function validate_currency_code( string $currency_code ) { $account_data = WC_Payments::get_account_service()->get_cached_account_data(); if ( isset( $account_data['customer_currencies']['supported'] ) && ! in_array( $currency_code, $account_data['customer_currencies']['supported'], true ) ) { throw new Invalid_Request_Parameter_Exception( - sprintf( - // Translators: %s is a currency code. - __( '%s is not a supported currency for payments.', 'woocommerce-payments' ), - $currency_code + esc_html( + sprintf( + // Translators: %s is a currency code. + __( '%s is not a supported currency for payments.', 'woocommerce-payments' ), + $currency_code + ) ), 'wcpay_core_invalid_request_parameter_currency_not_available' ); @@ -725,15 +737,16 @@ public function validate_extended_class( $child_class, string $parent_class ) { if ( ! is_subclass_of( $child_class, $parent_class ) ) { throw new Extend_Request_Exception( - sprintf( - 'Failed to extend request. %s is not a subclass of %s', - is_string( $child_class ) ? $child_class : get_class( $child_class ), - $parent_class + esc_html( + sprintf( + 'Failed to extend request. %s is not a subclass of %s', + is_string( $child_class ) ? $child_class : get_class( $child_class ), + $parent_class + ) ), 'wcpay_core_extend_class_not_subclass' ); } - } /** @@ -749,11 +762,13 @@ public function validate_date( string $date, string $format = 'Y-m-d H:i:s' ) { $d = DateTime::createFromFormat( $format, $date ); if ( ! ( $d && $d->format( $format ) === $date ) ) { throw new Invalid_Request_Parameter_Exception( - sprintf( + esc_html( + sprintf( // Translators: %1$s is a provided date string, %2$s is a date format. - __( '%1$s is not a valid date for format %2$s.', 'woocommerce-payments' ), - $date, - $format + __( '%1$s is not a valid date for format %2$s.', 'woocommerce-payments' ), + $date, + $format + ) ), 'wcpay_core_invalid_request_parameter_invalid_date' ); @@ -772,10 +787,12 @@ public function validate_redirect_url( string $redirect_url ) { $check_fallback_url = wp_generate_password( 12, false ); if ( hash_equals( $check_fallback_url, wp_validate_redirect( $redirect_url, $check_fallback_url ) ) ) { throw new Invalid_Request_Parameter_Exception( - sprintf( - // Translators: %s is a currency code. - __( '%1$s is not a valid redirect URL. Use a URL in the allowed_redirect_hosts filter.', 'woocommerce-payments' ), - $redirect_url + esc_html( + sprintf( + // Translators: %s is a currency code. + __( '%1$s is not a valid redirect URL. Use a URL in the allowed_redirect_hosts filter.', 'woocommerce-payments' ), + $redirect_url + ) ), 'wcpay_core_invalid_request_parameter_invalid_redirect_url' ); @@ -794,10 +811,12 @@ public function validate_user_name( string $user_name ) { $user = get_user_by( 'login', $user_name ); if ( false === $user ) { throw new Invalid_Request_Parameter_Exception( - sprintf( + esc_html( + sprintf( // Translators: %s is a provided username. - __( '%s is not a valid username.', 'woocommerce-payments' ), - $user_name + __( '%s is not a valid username.', 'woocommerce-payments' ), + $user_name + ) ), 'wcpay_core_invalid_request_parameter_invalid_username' ); @@ -821,6 +840,5 @@ public function validate_api_route( string $api_route ) { return; } throw new Invalid_Request_Parameter_Exception( 'Invalid request api route', 'wcpay_core_invalid_request_parameter_api_route_not_defined' ); - } } diff --git a/includes/core/server/class-response.php b/includes/core/server/class-response.php index ab56103bbe9..8357bc772be 100644 --- a/includes/core/server/class-response.php +++ b/includes/core/server/class-response.php @@ -36,7 +36,7 @@ public function __construct( array $data ) { * @param mixed $offset The key to check. * @return bool */ - public function offsetExists( $offset ) : bool { + public function offsetExists( $offset ): bool { return isset( $this->data[ $offset ] ); } diff --git a/includes/core/server/request/class-create-and-confirm-setup-intention.php b/includes/core/server/request/class-create-and-confirm-setup-intention.php index bf8831e293a..e38c581d7c4 100644 --- a/includes/core/server/request/class-create-and-confirm-setup-intention.php +++ b/includes/core/server/request/class-create-and-confirm-setup-intention.php @@ -102,7 +102,7 @@ public function set_payment_method_types( array $payment_methods ) { // Hard to validate without hardcoding a list here. if ( empty( $payment_methods ) ) { throw new Invalid_Request_Parameter_Exception( - __( 'Intentions require at least one payment method', 'woocommerce-payments' ), + esc_html__( 'Intentions require at least one payment method', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_missing_payment_method_types' ); } @@ -119,7 +119,6 @@ public function set_payment_method_types( array $payment_methods ) { */ public function set_mandate_data( array $mandate_data ) { $this->set_param( 'mandate_data', $mandate_data ); - } /** diff --git a/includes/core/server/request/class-generic.php b/includes/core/server/request/class-generic.php index 9e6b51ea8c5..36543990864 100644 --- a/includes/core/server/request/class-generic.php +++ b/includes/core/server/request/class-generic.php @@ -71,8 +71,6 @@ public function __construct( string $api, string $method, array $parameters = nu $this->set( $key, $value ); } } - - return $this; } /** diff --git a/includes/core/server/request/class-list-disputes.php b/includes/core/server/request/class-list-disputes.php index 721e35bac65..87519aa4abb 100644 --- a/includes/core/server/request/class-list-disputes.php +++ b/includes/core/server/request/class-list-disputes.php @@ -134,7 +134,7 @@ public function set_created_between( array $created_between ) { public function set_search( $search ) { if ( ! is_string( $search ) && ! is_array( $search ) ) { throw new Invalid_Request_Parameter_Exception( - __( 'The search parameter must be a string, or an array of strings.', 'woocommerce-payments' ), + esc_html__( 'The search parameter must be a string, or an array of strings.', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_invalid_search' ); } @@ -192,7 +192,4 @@ public function format_response( $response ) { return new Response( $response ); } - - - } diff --git a/includes/core/server/request/class-list-fraud-outcome-transactions.php b/includes/core/server/request/class-list-fraud-outcome-transactions.php index 4e15c21cf5f..d7d3770c7c5 100644 --- a/includes/core/server/request/class-list-fraud-outcome-transactions.php +++ b/includes/core/server/request/class-list-fraud-outcome-transactions.php @@ -232,7 +232,7 @@ private function get_search_result( $found, $term, $outcome ) { // Search by order id. if ( preg_match( '/#(\d+)/', $term, $matches ) ) { return $matches[1] === (string) $outcome['order_id']; - }; + } // Search by customer name. return (bool) preg_match( "/{$term}/i", $outcome['customer_name'] ); @@ -260,11 +260,11 @@ private function get_sort_result( $a, $b, $sort, $direction ) { if ( $a === $b ) { return 0; - }; + } if ( 'desc' === $direction ) { return $a < $b ? 1 : -1; - }; + } return $a < $b ? -1 : 1; } diff --git a/includes/core/server/request/class-list-transactions.php b/includes/core/server/request/class-list-transactions.php index baaa0154a92..4a2b998622e 100644 --- a/includes/core/server/request/class-list-transactions.php +++ b/includes/core/server/request/class-list-transactions.php @@ -19,7 +19,8 @@ */ class List_Transactions extends Paginated { - use Date_Parameters, Order_Info; + use Date_Parameters; + use Order_Info; const DEFAULT_PARAMS = [ 'sort' => 'date', @@ -309,5 +310,4 @@ public function format_response( $response ) { return new Response( $response ); } - } diff --git a/includes/core/server/request/class-request-utils.php b/includes/core/server/request/class-request-utils.php index 0090ad7ec90..aff6f59436d 100644 --- a/includes/core/server/request/class-request-utils.php +++ b/includes/core/server/request/class-request-utils.php @@ -46,5 +46,4 @@ public static function format_transaction_date_by_timezone( $transaction_date, $ return $formatted_date->format( 'Y-m-d H:i:s' ); } - } diff --git a/includes/core/server/request/class-update-account.php b/includes/core/server/request/class-update-account.php index 8a0eef57b7b..24dc72ae1e5 100644 --- a/includes/core/server/request/class-update-account.php +++ b/includes/core/server/request/class-update-account.php @@ -58,7 +58,7 @@ public function should_use_user_token(): bool { public static function from_account_settings( array $account_settings ) { if ( 0 === count( $account_settings ) ) { throw new Invalid_Request_Parameter_Exception( - __( 'No account settings provided', 'woocommerce-payments' ), + esc_html__( 'No account settings provided', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_account_settings_empty' ); } diff --git a/includes/core/server/request/class-woopay-create-and-confirm-intention.php b/includes/core/server/request/class-woopay-create-and-confirm-intention.php index 587f79b5b19..f1bdb75f9f2 100644 --- a/includes/core/server/request/class-woopay-create-and-confirm-intention.php +++ b/includes/core/server/request/class-woopay-create-and-confirm-intention.php @@ -43,5 +43,4 @@ public function set_has_woopay_subscription( $has = true ) { public function set_save_payment_method_to_platform( $save = true ) { $this->set_param( 'save_payment_method_to_platform', $save ); } - } diff --git a/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.php b/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.php index 350eb2103ac..eb04ecfdbe8 100644 --- a/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.php +++ b/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.php @@ -43,5 +43,4 @@ public function set_save_in_platform_account( $save = true ) { public function set_save_payment_method_to_platform( $save = true ) { $this->set_param( 'save_payment_method_to_platform', $save ); } - } diff --git a/includes/core/server/request/trait-date-parameters.php b/includes/core/server/request/trait-date-parameters.php index d933279c176..2565ecc8eef 100644 --- a/includes/core/server/request/trait-date-parameters.php +++ b/includes/core/server/request/trait-date-parameters.php @@ -59,5 +59,3 @@ public function set_date_between( array $date_between ) { } } } - - diff --git a/includes/core/server/request/trait-order-info.php b/includes/core/server/request/trait-order-info.php index 30f8b6cd9be..837e75cc7d2 100644 --- a/includes/core/server/request/trait-order-info.php +++ b/includes/core/server/request/trait-order-info.php @@ -65,5 +65,3 @@ private function get_customer_url( WC_Order $order ) { ); } } - - diff --git a/includes/core/service/class-wc-payments-customer-service-api.php b/includes/core/service/class-wc-payments-customer-service-api.php index 8431fcd18d5..b8d70deea58 100644 --- a/includes/core/service/class-wc-payments-customer-service-api.php +++ b/includes/core/service/class-wc-payments-customer-service-api.php @@ -10,8 +10,8 @@ use WCPay\Exceptions\API_Exception; use WC_Payments_Customer_Service; use WP_User; -use \WC_Customer; -use \WC_Order; +use WC_Customer; +use WC_Order; defined( 'ABSPATH' ) || exit; diff --git a/includes/exceptions/class-cannot-combine-currencies-exception.php b/includes/exceptions/class-cannot-combine-currencies-exception.php new file mode 100644 index 00000000000..84fcc12797f --- /dev/null +++ b/includes/exceptions/class-cannot-combine-currencies-exception.php @@ -0,0 +1,46 @@ +currency = $currency; + + parent::__construct( $message, 'cannot_combine_currencies', $http_code, null, null, $code, $previous ); + } + + /** + * Returns the currency of the minumum required amount. + * + * @return string + */ + public function get_currency() { + return $this->currency; + } +} 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 7f48ce2619b..6a16348b260 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 @@ -159,7 +159,7 @@ public function add_pay_for_order_params_to_js_config() { if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && current_user_can( 'pay_for_order', $order_id ) ) { add_filter( 'wcpay_payment_fields_js_config', - function( $js_config ) use ( $order ) { + function ( $js_config ) use ( $order ) { $session = wc()->session; $session_email = ''; @@ -172,8 +172,10 @@ function( $js_config ) use ( $order ) { // nosemgrep: audit.php.lang.misc.filter-input-no-filter. $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) : $session_email; - $js_config['order_id'] = $order->get_id(); + $js_config['order_id'] = $order->get_id(); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $js_config['pay_for_order'] = sanitize_text_field( wp_unslash( $_GET['pay_for_order'] ) ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $js_config['key'] = sanitize_text_field( wp_unslash( $_GET['key'] ) ); $js_config['billing_email'] = current_user_can( 'read_private_shop_orders' ) || ( get_current_user_id() !== 0 && $order->get_customer_id() === get_current_user_id() ) diff --git a/includes/fraud-prevention/class-fraud-risk-tools.php b/includes/fraud-prevention/class-fraud-risk-tools.php index 9b116ae980e..d79c2774959 100644 --- a/includes/fraud-prevention/class-fraud-risk-tools.php +++ b/includes/fraud-prevention/class-fraud-risk-tools.php @@ -7,13 +7,12 @@ namespace WCPay\Fraud_Prevention; -require_once dirname( __FILE__ ) . '/models/class-check.php'; -require_once dirname( __FILE__ ) . '/models/class-rule.php'; +require_once __DIR__ . '/models/class-check.php'; +require_once __DIR__ . '/models/class-rule.php'; use WC_Payments; use WC_Payments_Account; use WC_Payments_Features; -use WC_Payments_API_Client; use WCPay\Fraud_Prevention\Models\Check; use WCPay\Fraud_Prevention\Models\Rule; use WCPay\Constants\Currency_Code; @@ -126,6 +125,39 @@ public static function get_basic_protection_settings() { return self::get_ruleset_array( $rules ); } + /** + * Validates the array to see if it's a valid ruleset. + * + * @param array $array The array to validate. + * + * @return bool Whether if the given array is a ruleset, or not. + */ + public static function is_valid_ruleset_array( array $array ) { + foreach ( $array as $rule ) { + if ( ! Rule::validate_array( $rule ) ) { + return false; + } + } + return true; + } + + /** + * Returns the international IP address rule. + * + * @return Rule International IP address rule object. + */ + public static function get_international_ip_address_rule() { + return new Rule( + self::RULE_INTERNATIONAL_IP_ADDRESS, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, + Check::check( + 'ip_country', + self::get_selling_locations_type_operator(), + self::get_selling_locations_string() + ) + ); + } + /** * Returns the standard protection rules. * @@ -134,19 +166,11 @@ public static function get_basic_protection_settings() { public static function get_standard_protection_settings() { $rules = [ // REVIEW An order originates from an IP address outside your country. - new Rule( - self::RULE_INTERNATIONAL_IP_ADDRESS, - Rule::FRAUD_OUTCOME_REVIEW, - Check::check( - 'ip_country', - self::get_selling_locations_type_operator(), - self::get_selling_locations_string() - ) - ), + self::get_international_ip_address_rule(), // REVIEW An order exceeds $1,000.00 or 10 items. new Rule( self::RULE_ORDER_ITEMS_THRESHOLD, - Rule::FRAUD_OUTCOME_REVIEW, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'item_count', Check::OPERATOR_GT, @@ -156,7 +180,7 @@ public static function get_standard_protection_settings() { // REVIEW An order exceeds $1,000.00 or 10 items. new Rule( self::RULE_PURCHASE_PRICE_THRESHOLD, - Rule::FRAUD_OUTCOME_REVIEW, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'order_total', Check::OPERATOR_GT, @@ -166,7 +190,7 @@ public static function get_standard_protection_settings() { // REVIEW An order is originated from a different country than the shipping country. new Rule( self::RULE_IP_ADDRESS_MISMATCH, - Rule::FRAUD_OUTCOME_REVIEW, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'ip_billing_country_same', Check::OPERATOR_EQUALS, @@ -208,7 +232,7 @@ public static function get_high_protection_settings() { // REVIEW An order has less than 2 items or more than 10 items. new Rule( self::RULE_ORDER_ITEMS_THRESHOLD, - Rule::FRAUD_OUTCOME_REVIEW, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::list( Check::LIST_OPERATOR_OR, [ @@ -220,7 +244,7 @@ public static function get_high_protection_settings() { // REVIEW The shipping and billing address don't match. new Rule( self::RULE_ADDRESS_MISMATCH, - Rule::FRAUD_OUTCOME_REVIEW, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'billing_shipping_address_same', Check::OPERATOR_EQUALS, @@ -230,7 +254,7 @@ public static function get_high_protection_settings() { // REVIEW An order is originated from a different country than the shipping country. new Rule( self::RULE_IP_ADDRESS_MISMATCH, - Rule::FRAUD_OUTCOME_REVIEW, + WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'ip_billing_country_same', Check::OPERATOR_EQUALS, @@ -310,9 +334,9 @@ private static function get_selling_locations_string() { $selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' ); switch ( $selling_locations_type ) { case 'specific': - return implode( '|', get_option( 'woocommerce_specific_allowed_countries', [] ) ); + return strtolower( implode( '|', get_option( 'woocommerce_specific_allowed_countries', [] ) ) ); case 'all_except': - return implode( '|', get_option( 'woocommerce_all_except_countries', [] ) ); + return strtolower( implode( '|', get_option( 'woocommerce_all_except_countries', [] ) ) ); case 'all': return ''; default: diff --git a/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php b/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php index 98377aea4de..5b498ec90c8 100644 --- a/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php +++ b/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php @@ -137,7 +137,7 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) { } $callout = __( 'Learn more', 'woocommerce-payments' ); - $callout_url = 'https://woo.com/document/woopayments/fraud-and-disputes/fraud-protection/'; + $callout_url = 'https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/'; $callout_url = add_query_arg( 'status_is', 'fraud-meta-box-not-wcpay-learn-more', $callout_url ); echo '

' . esc_html( $description ) . '

' . esc_html( $callout ) . ''; break; diff --git a/includes/fraud-prevention/models/class-check.php b/includes/fraud-prevention/models/class-check.php index 1b56f8d0fca..39e2a557c1e 100644 --- a/includes/fraud-prevention/models/class-check.php +++ b/includes/fraud-prevention/models/class-check.php @@ -154,12 +154,14 @@ public static function validate_array( array $array ): bool { */ public static function list( string $operator, array $checks ) { if ( ! in_array( $operator, self::$list_operators, true ) ) { + // $operator is a predefined constant, no need to escape. + // phpcs:ignore WordPress.Security.EscapeOutput throw new Fraud_Ruleset_Exception( 'Operator for the check is invalid: ' . $operator ); } if ( 0 < count( array_filter( $checks, - function( $check ) { + function ( $check ) { return ! ( $check instanceof Check ); } ) ) ) { @@ -183,6 +185,8 @@ function( $check ) { */ public static function check( string $key, string $operator, $value ) { if ( ! in_array( $operator, self::$check_operators, true ) ) { + // $operator is a predefined constant, no need to escape. + // phpcs:ignore WordPress.Security.EscapeOutput throw new Fraud_Ruleset_Exception( 'Operator for the check is invalid: ' . $operator ); } @@ -203,7 +207,7 @@ public function to_array() { return [ 'operator' => $this->operator, 'checks' => array_map( - function( Check $check ) { + function ( Check $check ) { return $check->to_array(); }, $this->checks diff --git a/includes/in-person-payments/class-wc-payments-in-person-payments-receipts-service.php b/includes/in-person-payments/class-wc-payments-in-person-payments-receipts-service.php index af9d4d3b2e6..f24261a038a 100644 --- a/includes/in-person-payments/class-wc-payments-in-person-payments-receipts-service.php +++ b/includes/in-person-payments/class-wc-payments-in-person-payments-receipts-service.php @@ -21,7 +21,7 @@ class WC_Payments_In_Person_Payments_Receipts_Service { * * @return string */ - public function get_receipt_markup( array $settings, WC_Order $order, array $charge ) :string { + public function get_receipt_markup( array $settings, WC_Order $order, array $charge ): string { $this->validate_settings( $settings ); $this->validate_charge( $charge ); @@ -90,7 +90,7 @@ public function send_customer_ipp_receipt_email( WC_Order $order, array $merchan * @param array $order the order. * @return array */ - private function format_line_items( array $order ) :array { + private function format_line_items( array $order ): array { $line_items_data = []; foreach ( $order['line_items'] as $item ) { @@ -152,7 +152,6 @@ private function validate_charge( array $charge ) { $charge['payment_method_details']['card_present']['receipt'], 'Error validating receipt information' ); - } /** @@ -167,7 +166,7 @@ private function validate_charge( array $charge ) { private function validate_required_fields( array $required_fields, array $data, string $message ) { foreach ( $required_fields as $required_key ) { if ( ! array_key_exists( $required_key, $data ) ) { - throw new \RuntimeException( sprintf( '%s. Missing key: %s', $message, $required_key ) ); + throw new \RuntimeException( esc_html( sprintf( '%s. Missing key: %s', $message, $required_key ) ) ); } } } diff --git a/includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php b/includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php index f0a9a501ffa..33ec20f3a75 100644 --- a/includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php +++ b/includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php @@ -73,5 +73,4 @@ public function __construct() { public function get_data(): array { return self::PREVIEW_RECEIPT_ORDER_DATA; } - } diff --git a/includes/in-person-payments/templates/html-in-person-payment-receipt.php b/includes/in-person-payments/templates/html-in-person-payment-receipt.php index 1b370b05e5e..e575e52fd03 100644 --- a/includes/in-person-payments/templates/html-in-person-payment-receipt.php +++ b/includes/in-person-payments/templates/html-in-person-payment-receipt.php @@ -129,7 +129,7 @@ function format_price_helper( array $product, string $currency ): string {

-

+


@@ -140,7 +140,7 @@ function format_price_helper( array $product, string $currency ): string { @@ -157,7 +157,7 @@ function format_price_helper( array $product, string $currency ): string { @@ -211,9 +211,9 @@ function format_price_helper( array $product, string $currency ): string {
-

-

-

+

+

+

diff --git a/includes/migrations/class-allowed-payment-request-button-sizes-update.php b/includes/migrations/class-allowed-payment-request-button-sizes-update.php index 4be05fa2bf2..629d1114356 100644 --- a/includes/migrations/class-allowed-payment-request-button-sizes-update.php +++ b/includes/migrations/class-allowed-payment-request-button-sizes-update.php @@ -64,6 +64,5 @@ private function migrate() { 'small' ); } - } } diff --git a/includes/migrations/class-update-service-data-from-server.php b/includes/migrations/class-update-service-data-from-server.php index 10ecb0e5d9f..f674018619e 100644 --- a/includes/migrations/class-update-service-data-from-server.php +++ b/includes/migrations/class-update-service-data-from-server.php @@ -62,5 +62,4 @@ public function maybe_migrate() { private function migrate() { $this->account->refresh_account_data(); } - } diff --git a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php index c716e985acc..0095bc70559 100644 --- a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php +++ b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php @@ -106,7 +106,6 @@ public function convert_cart_currency( $cart_item, $values ) { } return $cart_item; - } /** diff --git a/includes/multi-currency/Currency.php b/includes/multi-currency/Currency.php index 238281a2049..23050339271 100644 --- a/includes/multi-currency/Currency.php +++ b/includes/multi-currency/Currency.php @@ -188,7 +188,7 @@ public function get_symbol(): string { * * @return string Currency position (left/right). */ - public function get_symbol_position() : string { + public function get_symbol_position(): string { $localization_service = new WC_Payments_Localization_Service(); return $localization_service->get_currency_format( $this->code )['currency_pos']; } diff --git a/includes/multi-currency/FrontendCurrencies.php b/includes/multi-currency/FrontendCurrencies.php index 0c5fdf46821..da1342ac55a 100644 --- a/includes/multi-currency/FrontendCurrencies.php +++ b/includes/multi-currency/FrontendCurrencies.php @@ -111,18 +111,12 @@ public function init_hooks() { // Currency hooks. add_filter( 'woocommerce_currency', [ $this, 'get_woocommerce_currency' ], 900 ); add_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 ); + add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 ); add_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 ); add_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 ); add_action( 'before_woocommerce_pay', [ $this, 'init_order_currency_from_query_vars' ] ); add_action( 'woocommerce_order_get_total', [ $this, 'maybe_init_order_currency_from_order_total_prop' ], 900, 2 ); add_action( 'woocommerce_get_formatted_order_total', [ $this, 'maybe_clear_order_currency_after_formatted_order_total' ], 900, 4 ); - - // Note: it's important that 'init_order_currency_from_query_vars' is called before - // 'get_price_decimal_separator' because the order currency is often required to - // determine the decimal separator. That's why the priority on 'init_order_currency_from_query_vars' - // is explicity lower than the priority of 'get_price_decimal_separator'. - add_filter( 'wc_get_price_decimal_separator', [ $this, 'init_order_currency_from_query_vars' ], 900 ); - add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 901 ); } add_filter( 'woocommerce_thankyou_order_id', [ $this, 'init_order_currency' ] ); @@ -276,16 +270,17 @@ public function init_order_currency( $arg ) { return $arg; } - // We remove the filters here becuase 'wc_get_order' triggers the 'wc_get_price_decimal_separator' filter. - remove_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 901 ); - remove_filter( 'wc_get_price_decimal_separator', [ $this, 'init_order_currency_from_query_vars' ], 900 ); + // We remove these filters here because 'wc_get_order' + // can trigger them, leading to an infinitely recursive call. + remove_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 ); + remove_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 ); + remove_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 ); + remove_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 ); $order = ! $arg instanceof WC_Order ? wc_get_order( $arg ) : $arg; - // Note: it's important that 'init_order_currency_from_query_vars' is called before - // 'get_price_decimal_separator' because the order currency is often required to - // determine the decimal separator. That's why the priority on 'init_order_currency_from_query_vars' - // is explicity lower than the priority of 'get_price_decimal_separator'. - add_filter( 'wc_get_price_decimal_separator', [ $this, 'init_order_currency_from_query_vars' ], 900 ); - add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 901 ); + add_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 ); + add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 ); + add_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 ); + add_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 ); if ( $order ) { $this->order_currency = $order->get_currency(); @@ -382,6 +377,8 @@ public function maybe_clear_order_currency_after_formatted_order_total( $formatt */ private function get_currency_code() { if ( $this->should_use_order_currency() ) { + $this->init_order_currency_from_query_vars(); + return $this->order_currency; } @@ -409,7 +406,7 @@ private function get_selected_currency_code(): string { */ private function should_use_order_currency(): bool { $pages = [ 'my-account', 'checkout' ]; - $vars = [ 'order-received', 'order-pay', 'order-received', 'orders', 'view-order' ]; + $vars = [ 'order-received', 'order-pay', 'orders', 'view-order' ]; if ( $this->utils->is_page_with_vars( $pages, $vars ) ) { return $this->utils->is_call_in_backtrace( diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php index 002b0633a13..49c07c0fe8e 100644 --- a/includes/multi-currency/MultiCurrency.php +++ b/includes/multi-currency/MultiCurrency.php @@ -314,6 +314,8 @@ public function init() { // Update the customer currencies option after an order status change. add_action( 'woocommerce_order_status_changed', [ $this, 'maybe_update_customer_currencies_option' ] ); + $this->maybe_add_cache_cookie(); + static::$is_initialized = true; } @@ -412,7 +414,7 @@ public function get_cached_currencies() { return $this->database_cache->get_or_add( Database_Cache::CURRENCIES_KEY, - function() { + function () { try { $currency_data = $this->payments_api_client->get_currency_rates( strtolower( get_woocommerce_currency() ) ); return [ @@ -561,7 +563,7 @@ public function update_single_currency_settings( string $currency_code, string $ if ( ! is_numeric( $manual_rate ) || 0 >= $manual_rate ) { $message = 'Invalid manual currency rate passed to update_single_currency_settings: ' . $manual_rate; Logger::error( $message ); - throw new InvalidCurrencyRateException( $message, 'wcpay_multi_currency_invalid_currency_rate', 500 ); + throw new InvalidCurrencyRateException( esc_html( $message ), 'wcpay_multi_currency_invalid_currency_rate', 500 ); } update_option( 'wcpay_multi_currency_manual_rate_' . $currency_code, $manual_rate ); } @@ -646,7 +648,7 @@ private function initialize_enabled_currencies() { // This allows to keep the alphabetical sorting by name. $enabled_currencies = array_filter( $available_currencies, - function( $currency ) use ( $enabled_currency_codes ) { + function ( $currency ) use ( $enabled_currency_codes ) { return in_array( $currency->get_code(), $enabled_currency_codes, true ); } ); @@ -813,6 +815,8 @@ public function update_selected_currency( string $currency_code, bool $persist_c } else { add_action( 'wp_loaded', [ $this, 'recalculate_cart' ] ); } + + $this->maybe_add_cache_cookie(); } /** @@ -935,7 +939,7 @@ public function get_raw_conversion( float $amount, string $to_currency, string $ if ( 0 >= $from_currency_rate ) { $message = 'Invalid rate for from_currency in get_raw_conversion: ' . $from_currency_rate; Logger::error( $message ); - throw new InvalidCurrencyRateException( $message, 'wcpay_multi_currency_invalid_currency_rate', 500 ); + throw new InvalidCurrencyRateException( esc_html( $message ), 'wcpay_multi_currency_invalid_currency_rate', 500 ); } $amount = $amount * ( $to_currency_rate / $from_currency_rate ); @@ -1019,6 +1023,8 @@ public function display_geolocation_currency_update_notice() { $notice_id = md5( $message ); echo '
http->is_connected() ? esc_html__( 'Yes', 'woocommerce-payments' ) : ' ' . esc_html__( 'No', 'woocommerce-payments' ) . ''; ?>
: http->is_connected() ? $this->http->get_blog_id() : '-' ); ?>
: gateway->is_connected() ? esc_html( $this->account->get_stripe_account_id() ?? '-' ) : ' ' . esc_html__( 'Not connected', 'woocommerce-payments' ) . ''; ?>
: gateway->needs_setup() ? ' ' . esc_html__( 'Needs setup', 'woocommerce-payments' ) . '' : ( $this->gateway->is_enabled() ? esc_html__( 'Enabled', 'woocommerce-payments' ) : esc_html__( 'Disabled', 'woocommerce-payments' ) ); ?>
: is_test() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?>
: gateway->get_upe_enabled_payment_method_ids() ) ); ?>
: gateway->get_option( 'platform_checkout_button_locations', [] ); @@ -169,14 +170,14 @@ public function render_status_report_section() { ?>
:
: gateway->get_option( 'payment_request' ); @@ -188,20 +189,20 @@ public function render_status_report_section() { ?>
: gateway->get_option( 'current_protection_level' ) ); ?>
: gateway->get_option( 'advanced_fraud_protection_settings' ) ), true ); $list = array_filter( array_map( - function( $rule ) { + function ( $rule ) { if ( empty( $rule['key'] ) ) { return null; } @@ -236,29 +237,29 @@ function( $rule ) {
:
:
:
:
:
@
-
+
$order['currency'] ] ), 'post' ); ?>
-
+
$order['currency'] ] ), 'post' ); ?>