diff --git a/changelog/chore-port-express-checkout-changes-to-tokenized-ece b/changelog/chore-port-express-checkout-changes-to-tokenized-ece new file mode 100644 index 00000000000..bda5ba0fe54 --- /dev/null +++ b/changelog/chore-port-express-checkout-changes-to-tokenized-ece @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: chore: port express checkout changes to tokenized ECE + + diff --git a/changelog/feat-google-pay-test-mode-live-account-compatibility-notice b/changelog/feat-google-pay-test-mode-live-account-compatibility-notice new file mode 100644 index 00000000000..5430e1defa2 --- /dev/null +++ b/changelog/feat-google-pay-test-mode-live-account-compatibility-notice @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +feat: add compatibility notice for Google Pay with live mode accounts. diff --git a/changelog/fix-pmme-on-cart-block b/changelog/fix-pmme-on-cart-block new file mode 100644 index 00000000000..97769b33edc --- /dev/null +++ b/changelog/fix-pmme-on-cart-block @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Init PMME container in cart block so that it can be dynamically rendered once the requirements are met. diff --git a/changelog/fix-style-extraction-in-editor b/changelog/fix-style-extraction-in-editor new file mode 100644 index 00000000000..dc191757fe1 --- /dev/null +++ b/changelog/fix-style-extraction-in-editor @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Properly extract styles when using the site editor. diff --git a/changelog/fix-wc9.7-ece-script-dependencies b/changelog/fix-wc9.7-ece-script-dependencies new file mode 100644 index 00000000000..f0d00730f68 --- /dev/null +++ b/changelog/fix-wc9.7-ece-script-dependencies @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: GooglePay/ApplePay script dependencies with WooCommerce 9.7 diff --git a/client/additional-methods-setup/upe-preview-methods-selector/test/add-payment-methods-task.test.js b/client/additional-methods-setup/upe-preview-methods-selector/__tests__/add-payment-methods-task.test.js similarity index 100% rename from client/additional-methods-setup/upe-preview-methods-selector/test/add-payment-methods-task.test.js rename to client/additional-methods-setup/upe-preview-methods-selector/__tests__/add-payment-methods-task.test.js diff --git a/client/additional-methods-setup/upe-preview-methods-selector/test/currency-information-for-methods.test.js b/client/additional-methods-setup/upe-preview-methods-selector/__tests__/currency-information-for-methods.test.js similarity index 100% rename from client/additional-methods-setup/upe-preview-methods-selector/test/currency-information-for-methods.test.js rename to client/additional-methods-setup/upe-preview-methods-selector/__tests__/currency-information-for-methods.test.js diff --git a/client/additional-methods-setup/upe-preview-methods-selector/test/setup-complete-task.test.js b/client/additional-methods-setup/upe-preview-methods-selector/__tests__/setup-complete-task.test.js similarity index 88% rename from client/additional-methods-setup/upe-preview-methods-selector/test/setup-complete-task.test.js rename to client/additional-methods-setup/upe-preview-methods-selector/__tests__/setup-complete-task.test.js index 81ae65f67af..cfb14e78e66 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/test/setup-complete-task.test.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/__tests__/setup-complete-task.test.js @@ -10,6 +10,7 @@ import WizardTaskContext from '../../wizard/task/context'; import SetupComplete from '../setup-complete-task'; import WizardContext from '../../wizard/wrapper/context'; import { useEnabledPaymentMethodIds } from '../../../data'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), @@ -18,16 +19,24 @@ jest.mock( '../../../data', () => ( { useEnabledPaymentMethodIds: jest.fn(), } ) ); +const renderWithSettingsProvider = ( ui ) => + render( + + { ui } + + ); + describe( 'SetupComplete', () => { beforeEach( () => { useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'bancontact', 'eps', 'ideal', 'p24', 'sepa_debit' ], () => null, ] ); + global.wcpaySettings = { featureFlags: { multiCurrency: true } }; } ); it( 'renders setup complete messaging when context value is undefined', () => { - render( + renderWithSettingsProvider( @@ -41,9 +50,11 @@ describe( 'SetupComplete', () => { } ); it( 'renders setup complete messaging when context value is `true`', () => { - render( + renderWithSettingsProvider( @@ -57,7 +68,7 @@ describe( 'SetupComplete', () => { } ); it( 'renders setup complete messaging when context value says that methods have not changed', () => { - render( + renderWithSettingsProvider( { [ 'card', 'ideal' ], () => null, ] ); - render( + renderWithSettingsProvider( { [ 'card', 'ideal' ], () => null, ] ); - render( + renderWithSettingsProvider( { [ 'card', ...additionalMethods ], () => null, ] ); - render( + renderWithSettingsProvider( method in paymentMethods -); - -if ( bnplPaymentMethods.length ) { - // Register BNPL site messaging on the cart block. - registerPlugin( 'bnpl-site-messaging', { - render: renderBNPLCartMessaging, - scope: 'woocommerce-checkout', - } ); -} +registerPlugin( 'bnpl-site-messaging', { + render: renderBNPLCartMessaging, + scope: 'woocommerce-checkout', +} ); diff --git a/client/checkout/blocks/payment-elements.js b/client/checkout/blocks/payment-elements.js index f782ba13545..3d89aec45bc 100644 --- a/client/checkout/blocks/payment-elements.js +++ b/client/checkout/blocks/payment-elements.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useEffect, useState, RawHTML } from '@wordpress/element'; +import { useEffect, useState, RawHTML, useRef } from '@wordpress/element'; import { Elements } from '@stripe/react-stripe-js'; // eslint-disable-next-line import/no-unresolved import { StoreNotice } from '@woocommerce/blocks-checkout'; @@ -20,6 +20,7 @@ import { getPaymentMethodTypes } from 'wcpay/checkout/utils/upe'; const PaymentElements = ( { api, ...props } ) => { const stripeForUPE = useStripeForUPE( api, props.paymentMethodId ); + const containerRef = useRef( null ); const [ errorMessage, setErrorMessage ] = useState( null ); const [ @@ -29,7 +30,8 @@ const PaymentElements = ( { api, ...props } ) => { const [ appearance, setAppearance ] = useState( getUPEConfig( 'wcBlocksUPEAppearance' ) ); - const [ fontRules ] = useState( getFontRulesFromPage() ); + const [ fontRules, setFontRules ] = useState( [] ); + const [ fingerprint, fingerprintErrorMessage ] = useFingerprint(); const amount = Number( getUPEConfig( 'cartTotal' ) ); const currency = getUPEConfig( 'currency' ).toLowerCase(); @@ -37,8 +39,18 @@ const PaymentElements = ( { api, ...props } ) => { useEffect( () => { async function generateUPEAppearance() { + if ( ! containerRef.current ) { + return; + } + setFontRules( + getFontRulesFromPage( containerRef.current.ownerDocument ) + ); // Generate UPE input styles. - let upeAppearance = getAppearance( 'blocks_checkout', false ); + let upeAppearance = getAppearance( + 'blocks_checkout', + false, + containerRef.current.ownerDocument + ); upeAppearance = await api.saveUPEAppearance( upeAppearance, 'blocks_checkout' @@ -61,46 +73,48 @@ const PaymentElements = ( { api, ...props } ) => { props.paymentMethodId, ] ); - if ( ! stripeForUPE ) { - return ; - } - return ( - - + - { paymentProcessorLoadErrorMessage?.error?.message && ( - - - - { - paymentProcessorLoadErrorMessage.error - .message - } - - - - ) } - - - + + { paymentProcessorLoadErrorMessage?.error?.message && ( + + + + { + paymentProcessorLoadErrorMessage.error + .message + } + + + + ) } + + + + + > ); }; diff --git a/client/checkout/blocks/payment-method-label.js b/client/checkout/blocks/payment-method-label.js index ecc339b0a32..7de075a51de 100644 --- a/client/checkout/blocks/payment-method-label.js +++ b/client/checkout/blocks/payment-method-label.js @@ -15,7 +15,7 @@ import { useStripeForUPE } from 'wcpay/hooks/use-stripe-async'; import { getUPEConfig } from 'wcpay/utils/checkout'; import { __ } from '@wordpress/i18n'; import './style.scss'; -import { useEffect, useMemo, useState } from '@wordpress/element'; +import { useEffect, useState, useRef } from '@wordpress/element'; import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles'; const paymentMethods = [ @@ -78,6 +78,7 @@ const PaymentMethodMessageWrapper = ( { }; export default ( { api, title, countries, iconLight, iconDark, upeName } ) => { + const containerRef = useRef( null ); const cartData = wp.data.select( 'wc/store/cart' ).getCartData(); const isTestMode = getUPEConfig( 'testMode' ); const [ appearance, setAppearance ] = useState( @@ -88,7 +89,7 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => { getUPEConfig( 'wcBlocksUPEAppearanceTheme' ) ); - const fontRules = useMemo( () => getFontRulesFromPage(), [] ); + const [ fontRules, setFontRules ] = useState( [] ); // Stripe expects the amount to be sent as the minor unit of 2 digits. const amount = parseInt( @@ -107,8 +108,18 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => { useEffect( () => { async function generateUPEAppearance() { + if ( ! containerRef.current ) { + return; + } + setFontRules( + getFontRulesFromPage( containerRef.current.ownerDocument ) + ); // Generate UPE input styles. - let upeAppearance = getAppearance( 'blocks_checkout', false ); + let upeAppearance = getAppearance( + 'blocks_checkout', + false, + containerRef.current.ownerDocument + ); upeAppearance = await api.saveUPEAppearance( upeAppearance, 'blocks_checkout' @@ -130,7 +141,7 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => { return ( <> - + { title } { isTestMode && ( diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js index 502cf370382..440c55fc5c6 100644 --- a/client/checkout/upe-styles/index.js +++ b/client/checkout/upe-styles/index.js @@ -50,7 +50,7 @@ export const appearanceSelectors = { pmmeRelativeTextSizeSelector: '.wc_payment_method > label', }, blocksCheckout: { - appendTarget: '#contact-fields', + appendTarget: '.wc-block-checkout__contact-fields', upeThemeInputSelector: '.wc-block-components-text-input #email', upeThemeLabelSelector: '.wc-block-components-text-input label', upeThemeTextSelectors: [ @@ -176,16 +176,17 @@ export const appearanceSelectors = { * Update selectors to use alternate if not present on DOM. * * @param {Object} selectors Object of selectors for updation. + * @param {Object} scope The document scope to search in. * * @return {Object} Updated selectors. */ - updateSelectors: function ( selectors ) { + updateSelectors: function ( selectors, scope ) { if ( selectors.hasOwnProperty( 'alternateSelectors' ) ) { Object.entries( selectors.alternateSelectors ).forEach( ( altSelector ) => { const [ key, value ] = altSelector; - if ( ! document.querySelector( selectors[ key ] ) ) { + if ( ! scope.querySelector( selectors[ key ] ) ) { selectors[ key ] = value; } } @@ -201,10 +202,11 @@ export const appearanceSelectors = { * Returns selectors based on checkout type. * * @param {boolean} elementsLocation The location of the elements. + * @param {Object} scope The document scope to search in. * * @return {Object} Selectors for checkout type specified. */ - getSelectors: function ( elementsLocation ) { + getSelectors: function ( elementsLocation, scope ) { let appearanceSelector = this.blocksCheckout; switch ( elementsLocation ) { @@ -230,7 +232,7 @@ export const appearanceSelectors = { return { ...this.default, - ...this.updateSelectors( appearanceSelector ), + ...this.updateSelectors( appearanceSelector, scope ), }; }, }; @@ -240,11 +242,12 @@ const hiddenElementsForUPE = { * Create hidden container for generating UPE styles. * * @param {string} elementID ID of element to create. + * @param {Object} scope The document scope to search in. * * @return {Object} Object of the created hidden container element. */ - getHiddenContainer: function ( elementID ) { - const hiddenDiv = document.createElement( 'div' ); + getHiddenContainer: function ( elementID, scope ) { + const hiddenDiv = scope.createElement( 'div' ); hiddenDiv.setAttribute( 'id', this.getIDFromSelector( elementID ) ); hiddenDiv.style.border = 0; hiddenDiv.style.clip = 'rect(0 0 0 0)'; @@ -261,12 +264,13 @@ const hiddenElementsForUPE = { * Create invalid element row for generating UPE styles. * * @param {string} elementType Type of element to create. - * @param {Array} classes Array of classes to be added to the element. Default: empty array. + * @param {Array} classes Array of classes to be added to the element. Default: empty array. + * @param {Object} scope The document scope to search in. * * @return {Object} Object of the created invalid row element. */ - createRow: function ( elementType, classes = [] ) { - const newRow = document.createElement( elementType ); + createRow: function ( elementType, classes = [], scope ) { + const newRow = scope.createElement( elementType ); if ( classes.length ) { newRow.classList.add( ...classes ); } @@ -276,12 +280,18 @@ const hiddenElementsForUPE = { /** * Append elements to target container. * - * @param {Object} appendTarget Element object where clone should be appended. + * @param {Object} appendTarget Element object where clone should be appended. * @param {string} elementToClone Selector of the element to be cloned. - * @param {string} newElementID Selector for the cloned element. + * @param {string} newElementID Selector for the cloned element. + * @param {Object} scope The document scope to search in. */ - appendClone: function ( appendTarget, elementToClone, newElementID ) { - const cloneTarget = document.querySelector( elementToClone ); + appendClone: function ( + appendTarget, + elementToClone, + newElementID, + scope + ) { + const cloneTarget = scope.querySelector( elementToClone ); if ( cloneTarget ) { const clone = cloneTarget.cloneNode( true ); clone.id = this.getIDFromSelector( newElementID ); @@ -309,11 +319,12 @@ const hiddenElementsForUPE = { * Initialize hidden fields to generate UPE styles. * * @param {boolean} elementsLocation The location of the elements. + * @param {Object} scope The scope of the elements. */ - init: function ( elementsLocation ) { + init: function ( elementsLocation, scope ) { const selectors = appearanceSelectors.getSelectors( elementsLocation ), - appendTarget = document.querySelector( selectors.appendTarget ), - elementToClone = document.querySelector( + appendTarget = scope.querySelector( selectors.appendTarget ), + elementToClone = scope.querySelector( selectors.upeThemeInputSelector ); @@ -323,70 +334,77 @@ const hiddenElementsForUPE = { } // Remove hidden container is already present on DOM. - if ( document.querySelector( selectors.hiddenContainer ) ) { - this.cleanup(); + if ( scope.querySelector( selectors.hiddenContainer ) ) { + this.cleanup( scope ); } // Create hidden container & append to target. const hiddenContainer = this.getHiddenContainer( - selectors.hiddenContainer + selectors.hiddenContainer, + scope ); appendTarget.appendChild( hiddenContainer ); // Create hidden valid row & append to hidden container. const hiddenValidRow = this.createRow( selectors.rowElement, - selectors.validClasses + selectors.validClasses, + scope ); hiddenContainer.appendChild( hiddenValidRow ); // Create hidden invalid row & append to hidden container. const hiddenInvalidRow = this.createRow( selectors.rowElement, - selectors.invalidClasses + selectors.invalidClasses, + scope ); hiddenContainer.appendChild( hiddenInvalidRow ); - // Clone & append target input to hidden valid row. + // Clone & append target input to hidden valid row. this.appendClone( hiddenValidRow, selectors.upeThemeInputSelector, - selectors.hiddenInput + selectors.hiddenInput, + scope ); // Clone & append target label to hidden valid row. this.appendClone( hiddenValidRow, selectors.upeThemeLabelSelector, - selectors.hiddenValidActiveLabel + selectors.hiddenValidActiveLabel, + scope ); - // Clone & append target input to hidden invalid row. + // Clone & append target input to hidden invalid row. this.appendClone( hiddenInvalidRow, selectors.upeThemeInputSelector, - selectors.hiddenInvalidInput + selectors.hiddenInvalidInput, + scope ); // Clone & append target label to hidden invalid row. this.appendClone( hiddenInvalidRow, selectors.upeThemeLabelSelector, - selectors.hiddenInvalidInput + selectors.hiddenInvalidInput, + scope ); // Remove transitions & focus on hidden element. - const wcpayHiddenInput = document.querySelector( - selectors.hiddenInput - ); + const wcpayHiddenInput = scope.querySelector( selectors.hiddenInput ); wcpayHiddenInput.style.transition = 'none'; }, /** - * Remove hidden container from DROM. + * Remove hidden container from DOM. + * + * @param {Object} scope The scope of the elements. */ - cleanup: function () { - const element = document.querySelector( + cleanup: function ( scope ) { + const element = scope.querySelector( appearanceSelectors.default.hiddenContainer ); if ( element ) { @@ -398,17 +416,20 @@ const hiddenElementsForUPE = { export const getFieldStyles = ( selector, upeElement, - backgroundColor = null + backgroundColor = null, + scope ) => { - if ( ! document.querySelector( selector ) ) { + if ( ! scope.querySelector( selector ) ) { return {}; } + const windowObject = scope.defaultView || window; + const validProperties = upeRestrictedProperties[ upeElement ]; - const elem = document.querySelector( selector ); + const elem = scope.querySelector( selector ); - const styles = window.getComputedStyle( elem ); + const styles = windowObject.getComputedStyle( elem ); const filteredStyles = {}; for ( let i = 0; i < styles.length; i++ ) { @@ -455,9 +476,9 @@ export const getFieldStyles = ( return filteredStyles; }; -export const getFontRulesFromPage = () => { +export const getFontRulesFromPage = ( scope = document ) => { const fontRules = [], - sheets = document.styleSheets, + sheets = scope.styleSheets, fontDomains = [ 'fonts.googleapis.com', 'fonts.gstatic.com', @@ -485,13 +506,15 @@ export const getFontRulesFromPage = () => { * @param {string} selector Selector of the element to be checked. * @param {string} fontSize Pre-computed font size. * @param {number} percentage Percentage (0-1) to be used relative to the font size of the target element. + * @param {Object} scope The scope of the elements. * * @return {string} Font size of the element. */ function ensureFontSizeSmallerThan( selector, fontSize, - percentage = PMME_RELATIVE_TEXT_SIZE + percentage = PMME_RELATIVE_TEXT_SIZE, + scope ) { const fontSizeNumber = parseFloat( fontSize ); @@ -500,7 +523,7 @@ function ensureFontSizeSmallerThan( } // If the element is not found, return the font size number multiplied by the percentage. - const elem = document.querySelector( selector ); + const elem = scope.querySelector( selector ); if ( ! elem ) { return `${ fontSizeNumber * percentage }px`; } @@ -520,21 +543,37 @@ function ensureFontSizeSmallerThan( return `${ fontSizeNumber }px`; } -export const getAppearance = ( elementsLocation, forWooPay = false ) => { - const selectors = appearanceSelectors.getSelectors( elementsLocation ); +export const getAppearance = ( + elementsLocation, + forWooPay = false, + scope = document +) => { + const selectors = appearanceSelectors.getSelectors( + elementsLocation, + scope + ); // Add hidden fields to DOM for generating styles. - hiddenElementsForUPE.init( elementsLocation ); + hiddenElementsForUPE.init( elementsLocation, scope ); - const inputRules = getFieldStyles( selectors.hiddenInput, '.Input' ); + const inputRules = getFieldStyles( + selectors.hiddenInput, + '.Input', + null, + scope + ); const inputInvalidRules = getFieldStyles( selectors.hiddenInvalidInput, - '.Input' + '.Input', + null, + scope ); const labelRules = getFieldStyles( selectors.upeThemeLabelSelector, - '.Label' + '.Label', + null, + scope ); const labelRestingRules = { @@ -543,13 +582,22 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { const paragraphRules = getFieldStyles( selectors.upeThemeTextSelectors, - '.Text' + '.Text', + null, + scope ); - const tabRules = getFieldStyles( selectors.upeThemeInputSelector, '.Tab' ); + const tabRules = getFieldStyles( + selectors.upeThemeInputSelector, + '.Tab', + null, + scope + ); const selectedTabRules = getFieldStyles( selectors.hiddenInput, - '.Tab--selected' + '.Tab--selected', + null, + scope ); const tabHoverRules = generateHoverRules( tabRules ); @@ -560,24 +608,57 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { color: selectedTabRules.color, }; - const backgroundColor = getBackgroundColor( selectors.backgroundSelectors ); - const headingRules = getFieldStyles( selectors.headingSelectors, '.Label' ); + const backgroundColor = getBackgroundColor( + selectors.backgroundSelectors, + scope + ); + const headingRules = getFieldStyles( + selectors.headingSelectors, + '.Label', + null, + scope + ); const blockRules = getFieldStyles( selectors.upeThemeLabelSelector, '.Block', - backgroundColor + backgroundColor, + scope + ); + const buttonRules = getFieldStyles( + selectors.buttonSelectors, + '.Input', + null, + scope + ); + const linkRules = getFieldStyles( + selectors.linkSelectors, + '.Label', + null, + scope ); - const buttonRules = getFieldStyles( selectors.buttonSelectors, '.Input' ); - const linkRules = getFieldStyles( selectors.linkSelectors, '.Label' ); const containerRules = getFieldStyles( selectors.containerSelectors, - '.Container' + '.Container', + null, + scope + ); + const headerRules = getFieldStyles( + selectors.headerSelectors, + '.Header', + null, + scope + ); + const footerRules = getFieldStyles( + selectors.footerSelectors, + '.Footer', + null, + scope ); - const headerRules = getFieldStyles( selectors.headerSelectors, '.Header' ); - const footerRules = getFieldStyles( selectors.footerSelectors, '.Footer' ); const footerLinkRules = getFieldStyles( selectors.footerLink, - '.Footer--link' + '.Footer--link', + null, + scope ); const globalRules = { colorBackground: backgroundColor, @@ -589,7 +670,9 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { if ( selectors.pmmeRelativeTextSizeSelector && globalRules.fontSizeBase ) { globalRules.fontSizeBase = ensureFontSizeSmallerThan( selectors.pmmeRelativeTextSizeSelector, - paragraphRules.fontSize + paragraphRules.fontSize, + PMME_RELATIVE_TEXT_SIZE, + scope ); } @@ -623,7 +706,9 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { appearance, getFieldStyles( selectors.hiddenValidActiveLabel, - '.Label--floating' + '.Label--floating', + null, + scope ) ); } @@ -642,6 +727,6 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { } // Remove hidden fields from DOM. - hiddenElementsForUPE.cleanup(); + hiddenElementsForUPE.cleanup( scope ); return appearance; }; diff --git a/client/checkout/upe-styles/test/index.js b/client/checkout/upe-styles/test/index.js index 4af3358484c..3df8e81ba1f 100644 --- a/client/checkout/upe-styles/test/index.js +++ b/client/checkout/upe-styles/test/index.js @@ -32,16 +32,18 @@ describe( 'Getting styles for automated theming', () => { }; test( 'getFieldStyles returns correct styles for inputs', () => { - jest.spyOn( document, 'querySelector' ).mockImplementation( () => { - return mockElement; - } ); - jest.spyOn( window, 'getComputedStyle' ).mockImplementation( () => { - return mockCSStyleDeclaration; - } ); + const scope = { + querySelector: jest.fn( () => mockElement ), + defaultView: { + getComputedStyle: jest.fn( () => mockCSStyleDeclaration ), + }, + }; const fieldStyles = upeStyles.getFieldStyles( '.woocommerce-checkout .form-row input', - '.Input' + '.Input', + null, + scope ); expect( fieldStyles ).toEqual( { backgroundColor: 'rgba(0, 0, 0, 0)', @@ -55,13 +57,15 @@ describe( 'Getting styles for automated theming', () => { } ); test( 'getFieldStyles returns empty object if it can not find the element', () => { - jest.spyOn( document, 'querySelector' ).mockImplementation( () => { - return undefined; - } ); + const scope = { + querySelector: jest.fn( () => undefined ), + }; const fieldStyles = upeStyles.getFieldStyles( '.i-do-not-exist', - '.Input' + '.Input', + null, + scope ); expect( fieldStyles ).toEqual( {} ); } ); @@ -103,25 +107,31 @@ describe( 'Getting styles for automated theming', () => { }, 1: { href: null }, }; - jest.spyOn( document, 'styleSheets', 'get' ).mockReturnValue( - mockStyleSheets - ); + const scope = { + styleSheets: { + get: jest.fn( () => mockStyleSheets ), + }, + }; - const fontRules = upeStyles.getFontRulesFromPage(); + const fontRules = upeStyles.getFontRulesFromPage( scope ); expect( fontRules ).toEqual( [] ); } ); test( 'getAppearance returns the object with filtered CSS rules for UPE theming', () => { - jest.spyOn( document, 'querySelector' ).mockImplementation( () => { - return mockElement; - } ); - jest.spyOn( window, 'getComputedStyle' ).mockImplementation( () => { - return mockCSStyleDeclaration; - } ); + const scope = { + querySelector: jest.fn( () => mockElement ), + createElement: jest.fn( ( htmlTag ) => + document.createElement( htmlTag ) + ), + defaultView: { + getComputedStyle: jest.fn( () => mockCSStyleDeclaration ), + }, + }; const appearance = upeStyles.getAppearance( 'shortcode_checkout', - true + true, + scope ); expect( appearance ).toEqual( { variables: { @@ -285,23 +295,24 @@ describe( 'Getting styles for automated theming', () => { ], }, ].forEach( ( { elementsLocation, expectedSelectors } ) => { - afterEach( () => { - document.querySelector.mockClear(); - } ); - describe( `when elementsLocation is ${ elementsLocation }`, () => { test( 'getAppearance uses the correct appearanceSelectors based on the elementsLocation', () => { - jest.spyOn( document, 'querySelector' ).mockImplementation( - () => mockElement - ); - jest.spyOn( window, 'getComputedStyle' ).mockImplementation( - () => mockCSStyleDeclaration - ); + const scope = { + querySelector: jest.fn( () => mockElement ), + createElement: jest.fn( ( htmlTag ) => + document.createElement( htmlTag ) + ), + defaultView: { + getComputedStyle: jest.fn( + () => mockCSStyleDeclaration + ), + }, + }; - upeStyles.getAppearance( elementsLocation ); + upeStyles.getAppearance( elementsLocation, false, scope ); expectedSelectors.forEach( ( selector ) => { - expect( document.querySelector ).toHaveBeenCalledWith( + expect( scope.querySelector ).toHaveBeenCalledWith( selector ); } ); diff --git a/client/checkout/upe-styles/utils.js b/client/checkout/upe-styles/utils.js index 7546a899653..0f02995b5fb 100644 --- a/client/checkout/upe-styles/utils.js +++ b/client/checkout/upe-styles/utils.js @@ -105,21 +105,25 @@ export const dashedToCamelCase = ( string ) => { /** * Searches through array of CSS selectors and returns first visible background color. * - * @param {Array} selectors List of CSS selectors to check. + * @param {Array} selectors List of CSS selectors to check. + * @param {Object} scope The document scope to search in. * @return {string} CSS color value. */ -export const getBackgroundColor = ( selectors ) => { +export const getBackgroundColor = ( selectors, scope = document ) => { const defaultColor = '#ffffff'; let color = null; let i = 0; while ( ! color && i < selectors.length ) { - const element = document.querySelector( selectors[ i ] ); + const element = scope.querySelector( selectors[ i ] ); if ( ! element ) { i++; continue; } - const bgColor = window.getComputedStyle( element ).backgroundColor; + const windowObject = scope.defaultView || window; + + const bgColor = windowObject.getComputedStyle( element ) + .backgroundColor; // If backgroundColor property present and alpha > 0. if ( bgColor && tinycolor( bgColor ).getAlpha() > 0 ) { color = bgColor; diff --git a/client/components/deposits-overview/deposit-notices.tsx b/client/components/deposits-overview/deposit-notices.tsx index 7a65a9a8e6f..98954d776c4 100644 --- a/client/components/deposits-overview/deposit-notices.tsx +++ b/client/components/deposits-overview/deposit-notices.tsx @@ -205,7 +205,7 @@ export const DepositFailureNotice: React.FC< { /** * The link to update the account details. */ - updateAccountLink: string; + updateAccountLink?: string; } > = ( { updateAccountLink } ) => { const accountLinkWithSource = updateAccountLink ? addQueryArgs( updateAccountLink, { diff --git a/client/components/payment-methods-checkboxes/test/index.test.tsx b/client/components/payment-methods-checkboxes/__tests__/index.test.js similarity index 90% rename from client/components/payment-methods-checkboxes/test/index.test.tsx rename to client/components/payment-methods-checkboxes/__tests__/index.test.js index e55d7c11816..378d3e9e1c5 100644 --- a/client/components/payment-methods-checkboxes/test/index.test.tsx +++ b/client/components/payment-methods-checkboxes/__tests__/index.test.js @@ -2,9 +2,10 @@ /** * External dependencies */ -import React, { ReactNode } from 'react'; +import React from 'react'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { act } from 'react-dom/test-utils'; /** * Internal dependencies @@ -12,23 +13,30 @@ import userEvent from '@testing-library/user-event'; import PaymentMethodsCheckboxes from '..'; import PaymentMethodsCheckbox from '../payment-method-checkbox'; import { upeCapabilityStatuses } from '../../../additional-methods-setup/constants'; -import { act } from 'react-dom/test-utils'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; jest.mock( '@woocommerce/components', () => { return { - Pill: ( { - className, - children, - }: { - className: string; - children: ReactNode; - } ): React.ReactElement => ( + Pill: ( { className, children } ) => ( { children } ), }; } ); +const renderWithSettingsProvider = ( ui ) => + render( + + { ui } + + ); + describe( 'PaymentMethodsCheckboxes', () => { + beforeEach( () => { + global.wcpaySettings = { + accountFees: {}, + }; + } ); + it( 'triggers the onChange when clicking the checkbox', () => { const handleChange = jest.fn(); @@ -40,14 +48,14 @@ describe( 'PaymentMethodsCheckboxes', () => { [ 'sepa_debit', false ], ]; - render( + renderWithSettingsProvider( - { upeMethods.map( ( key ) => ( + { upeMethods.map( ( [ name, checked ] ) => ( { it( 'can click the checkbox on payment methods with pending statuses', () => { const handleChange = jest.fn(); - render( + renderWithSettingsProvider( { it( 'shows the required label on payment methods which are required', () => { const handleChange = jest.fn(); - const page = render( + const page = renderWithSettingsProvider( { it( 'shows the disabled notice pill on payment methods with disabled statuses', () => { const handleChange = jest.fn(); - const page = render( + const page = renderWithSettingsProvider( { it( 'can not click the payment methods checkbox that are locked', () => { const handleChange = jest.fn(); - render( + renderWithSettingsProvider( { it( 'can not click the payment methods checkbox with disabled statuses', () => { const handleChange = jest.fn(); - render( + renderWithSettingsProvider( { it( "doesn't show the disabled notice pill on payment methods with active and unrequested statuses", () => { const handleChange = jest.fn(); - render( + renderWithSettingsProvider( ; fraudServices: unknown[]; testMode: boolean; testModeOnboarding: boolean; @@ -26,9 +31,10 @@ declare global { isJetpackIdcActive: boolean; isAccountConnected: boolean; isAccountValid: boolean; - accountStatus: { + accountStatus: Partial< { email?: string; created: string; + isLive?: boolean; error?: boolean; status?: string; country?: string; @@ -69,7 +75,7 @@ declare global { declineOnAVSFailure: boolean; declineOnCVCFailure: boolean; }; - }; + } >; accountLoans: { has_active_loan: boolean; has_past_loans: boolean; diff --git a/client/overview/task-list/tasks.tsx b/client/overview/task-list/tasks.tsx index de13be15b7e..7555567aabf 100644 --- a/client/overview/task-list/tasks.tsx +++ b/client/overview/task-list/tasks.tsx @@ -103,7 +103,7 @@ export const getTasks = ( { getUpdateBusinessDetailsTask( errorMessages, status ?? '', - accountLink, + accountLink ?? '', Number( currentDeadline ) ?? null, pastDue ?? false, detailsSubmitted ?? true diff --git a/client/overview/task-list/tasks/po-task.tsx b/client/overview/task-list/tasks/po-task.tsx index 194363b47a9..a6d5eacc125 100644 --- a/client/overview/task-list/tasks/po-task.tsx +++ b/client/overview/task-list/tasks/po-task.tsx @@ -21,9 +21,9 @@ export const getVerifyBankAccountTask = (): any => { progressiveOnboarding: { isEnabled: poEnabled, isComplete: poComplete, - tpv, + tpv = 0, firstTransactionDate: firstPaymentDate, - }, + } = {}, created: createdDate, } = wcpaySettings.accountStatus; diff --git a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap b/client/payment-details/summary/__tests__/__snapshots__/index.test.js.snap similarity index 100% rename from client/payment-details/summary/test/__snapshots__/index.test.tsx.snap rename to client/payment-details/summary/__tests__/__snapshots__/index.test.js.snap diff --git a/client/payment-details/summary/test/index.test.tsx b/client/payment-details/summary/__tests__/index.test.js similarity index 90% rename from client/payment-details/summary/test/index.test.tsx rename to client/payment-details/summary/__tests__/index.test.js index a031a020aee..8daed75e2ab 100755 --- a/client/payment-details/summary/test/index.test.tsx +++ b/client/payment-details/summary/__tests__/index.test.js @@ -11,11 +11,10 @@ import '@wordpress/jest-console'; /** * Internal dependencies */ -import type { Charge } from 'wcpay/types/charges'; -import type { Dispute } from 'wcpay/types/disputes'; import PaymentDetailsSummary from '../'; import { useAuthorization } from 'wcpay/data'; import { paymentIntentMock } from 'wcpay/data/payment-intents/test/hooks'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; // Mock dateI18n jest.mock( '@wordpress/date', () => ( { @@ -26,28 +25,6 @@ jest.mock( '@wordpress/date', () => ( { } ), } ) ); -declare const global: { - wcSettings: { - locale: { - siteLocale: string; - }; - }; - wcpaySettings: { - isSubscriptionsActive: boolean; - shouldUseExplicitPrice: boolean; - zeroDecimalCurrencies: string[]; - currencyData: Record< string, any >; - connect: { - country: string; - }; - featureFlags: { - isAuthAndCaptureEnabled: boolean; - }; - dateFormat: string; - timeFormat: string; - }; -}; - const mockDisputeDoAccept = jest.fn(); jest.mock( 'wcpay/data', () => ( { @@ -76,88 +53,84 @@ jest.mock( '@wordpress/data', () => ( { withSelect: jest.fn( () => jest.fn() ), } ) ); -const mockUseAuthorization = useAuthorization as jest.MockedFunction< - typeof useAuthorization ->; - -const getBaseCharge = (): Charge => - ( { - id: 'ch_38jdHA39KKA', - payment_intent: 'pi_abc', - /* Stripe data comes in seconds, instead of the default Date milliseconds */ - created: 1568913840, +const mockUseAuthorization = useAuthorization; + +const getBaseCharge = () => ( { + id: 'ch_38jdHA39KKA', + payment_intent: 'pi_abc', + /* Stripe data comes in seconds, instead of the default Date milliseconds */ + created: 1568913840, + amount: 2000, + amount_refunded: 0, + application_fee_amount: 70, + disputed: false, + dispute: null, + currency: 'usd', + type: 'charge', + status: 'succeeded', + paid: true, + captured: true, + balance_transaction: { amount: 2000, - amount_refunded: 0, - application_fee_amount: 70, - disputed: false, - dispute: null, currency: 'usd', - type: 'charge', - status: 'succeeded', - paid: true, - captured: true, - balance_transaction: { - amount: 2000, - currency: 'usd', - fee: 70, - }, - refunds: { - data: [], - }, - order: { - number: 45981, - url: 'https://somerandomorderurl.com/?edit_order=45981', + fee: 70, + }, + refunds: { + data: [], + }, + order: { + number: 45981, + url: 'https://somerandomorderurl.com/?edit_order=45981', + }, + billing_details: { + name: 'Customer name', + email: 'mock@example.com', + }, + payment_method_details: { + card: { + brand: 'visa', + last4: '4242', }, - billing_details: { - name: 'Customer name', - email: 'mock@example.com', - }, - payment_method_details: { - card: { - brand: 'visa', - last4: '4242', - }, - type: 'card', - }, - outcome: { - risk_level: 'normal', - }, - } as any ); + type: 'card', + }, + outcome: { + risk_level: 'normal', + }, +} ); -const getBaseDispute = (): Dispute => - ( { - id: 'dp_1', - amount: 2000, - charge: 'ch_38jdHA39KKA', - order: null, - balance_transactions: [ - { - amount: -2000, - currency: 'usd', - fee: 1500, - reporting_category: 'dispute', - }, - ], - created: 1693453017, - currency: 'usd', - evidence: { - billing_address: '123 test address', - customer_email_address: 'test@email.com', - customer_name: 'Test customer', - shipping_address: '123 test address', - }, - evidence_details: { - due_by: 1694303999, - has_evidence: false, - past_due: false, - submission_count: 0, +const getBaseDispute = () => ( { + id: 'dp_1', + amount: 2000, + charge: 'ch_38jdHA39KKA', + order: null, + balance_transactions: [ + { + amount: -2000, + currency: 'usd', + fee: 1500, + reporting_category: 'dispute', }, - issuer_evidence: null, - metadata: {}, - payment_intent: 'pi_1', - reason: 'fraudulent', - status: 'needs_response', - } as Dispute ); + ], + created: 1693453017, + currency: 'usd', + evidence: { + billing_address: '123 test address', + customer_email_address: 'test@email.com', + customer_name: 'Test customer', + shipping_address: '123 test address', + }, + evidence_details: { + due_by: 1694303999, + has_evidence: false, + past_due: false, + submission_count: 0, + }, + issuer_evidence: null, + metadata: {}, + payment_intent: 'pi_1', + reason: 'fraudulent', + status: 'needs_response', +} ); const getBaseMetadata = () => ( { platform: 'ios', @@ -165,19 +138,16 @@ const getBaseMetadata = () => ( { reader_model: 'COTS_DEVICE', } ); -function renderCharge( - charge: Charge, - metadata = {}, - isLoading = false, - props = {} -) { +function renderCharge( charge, metadata = {}, isLoading = false, props = {} ) { const { container } = render( - + + + ); return container; } @@ -233,6 +203,7 @@ describe( 'PaymentDetailsSummary', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect( console ).toHaveWarnedWith( + // eslint-disable-next-line max-len 'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.' ); } ); @@ -251,7 +222,7 @@ describe( 'PaymentDetailsSummary', () => { balance_transaction: { amount: -charge.amount_refunded, currency: 'usd', - } as any, + }, } ); expect( renderCharge( charge ) ).toMatchSnapshot(); @@ -265,7 +236,7 @@ describe( 'PaymentDetailsSummary', () => { balance_transaction: { amount: -charge.amount_refunded, currency: 'usd', - } as any, + }, } ); const container = renderCharge( charge ); @@ -297,7 +268,7 @@ describe( 'PaymentDetailsSummary', () => { } ); test( 'renders loading state', () => { - expect( renderCharge( {} as any, true ) ).toMatchSnapshot(); + expect( renderCharge( {}, true ) ).toMatchSnapshot(); } ); describe( 'capture notification and fraud buttons', () => { @@ -545,7 +516,7 @@ describe( 'PaymentDetailsSummary', () => { reporting_category: 'dispute', }, ], - } as Dispute, + }, }; renderCharge( charge ); diff --git a/client/settings/__tests__/google-pay-test-mode-compatibility-notice.test.js b/client/settings/__tests__/google-pay-test-mode-compatibility-notice.test.js new file mode 100644 index 00000000000..c910f8f9373 --- /dev/null +++ b/client/settings/__tests__/google-pay-test-mode-compatibility-notice.test.js @@ -0,0 +1,88 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import GooglePayTestModeCompatibilityNotice from '../google-pay-test-mode-compatibility-notice'; +import { useTestMode, usePaymentRequestEnabledSettings } from 'wcpay/data'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; + +jest.mock( 'wcpay/data', () => ( { + useTestMode: jest.fn(), + usePaymentRequestEnabledSettings: jest.fn(), +} ) ); + +describe( 'GooglePayTestModeCompatibilityNotice', () => { + beforeEach( () => { + jest.resetAllMocks(); + } ); + + it( 'does not render when the account is not live', () => { + useTestMode.mockReturnValue( [ true ] ); + usePaymentRequestEnabledSettings.mockReturnValue( [ true ] ); + + const { container } = render( + + + + ); + + expect( container.firstChild ).toBeNull(); + } ); + + it( 'does not render when test mode is not active', () => { + useTestMode.mockReturnValue( [ false ] ); + usePaymentRequestEnabledSettings.mockReturnValue( [ true ] ); + + const { container } = render( + + + + ); + + expect( container.firstChild ).toBeNull(); + } ); + + it( 'does not render when Google Pay is not enabled', () => { + useTestMode.mockReturnValue( [ true ] ); + usePaymentRequestEnabledSettings.mockReturnValue( [ false ] ); + + const { container } = render( + + + + ); + + expect( container.firstChild ).toBeNull(); + } ); + + it( 'renders the notice when the requirements are met', () => { + useTestMode.mockReturnValue( [ true ] ); + usePaymentRequestEnabledSettings.mockReturnValue( [ true ] ); + + render( + + + + ); + + expect( + screen.getByText( + /Google Pay is incompatible with test mode when using production credentials/, + { ignore: '.a11y-speak-region' } + ) + ).toBeInTheDocument(); + } ); +} ); diff --git a/client/settings/test/settings-section.tsx b/client/settings/__tests__/settings-section.tsx similarity index 100% rename from client/settings/test/settings-section.tsx rename to client/settings/__tests__/settings-section.tsx diff --git a/client/settings/express-checkout-settings/test/file-upload.test.js b/client/settings/express-checkout-settings/__tests__/file-upload.test.js similarity index 100% rename from client/settings/express-checkout-settings/test/file-upload.test.js rename to client/settings/express-checkout-settings/__tests__/file-upload.test.js diff --git a/client/settings/express-checkout-settings/test/index.js b/client/settings/express-checkout-settings/__tests__/index.test.js similarity index 80% rename from client/settings/express-checkout-settings/test/index.js rename to client/settings/express-checkout-settings/__tests__/index.test.js index 899d5d782d5..c3377aa447c 100644 --- a/client/settings/express-checkout-settings/test/index.js +++ b/client/settings/express-checkout-settings/__tests__/index.test.js @@ -10,8 +10,10 @@ import { render, screen, within } from '@testing-library/react'; */ import ExpressCheckoutSettings from '..'; import PaymentRequestButtonPreview from '../payment-request-button-preview'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; jest.mock( '../../../data', () => ( { + useTestMode: jest.fn().mockReturnValue( [] ), useGetSettings: jest.fn().mockReturnValue( {} ), useSettings: jest.fn().mockReturnValue( {} ), usePaymentRequestEnabledSettings: jest @@ -66,9 +68,17 @@ jest.mock( '@woocommerce/components', () => ( { ) ), } ) ); +const renderWithSettingsProvider = ( ui ) => + render( + + { ui } + + ); + describe( 'ExpressCheckoutSettings', () => { beforeEach( () => { global.wcpaySettings = { + accountStatus: {}, restUrl: 'http://example.com/wp-json/', featureFlags: { woopayExpressCheckout: true, @@ -77,7 +87,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders error message for invalid method IDs', () => { - render( ); + renderWithSettingsProvider( + + ); const errorMessage = screen.queryByText( 'Invalid express checkout method ID specified.' @@ -86,7 +98,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders payment request breadcrumbs', () => { - render( ); + renderWithSettingsProvider( + + ); const linkToPayments = screen.getByRole( 'link', { name: 'WooPayments', @@ -100,7 +114,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders payment request title and description', () => { - render( ); + renderWithSettingsProvider( + + ); const heading = screen.queryByRole( 'heading', { name: 'Settings', @@ -109,7 +125,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders payment request enable setting and confirm its checkbox label', () => { - render( ); + renderWithSettingsProvider( + + ); const label = screen.getByRole( 'checkbox', { name: 'Enable Apple Pay / Google Pay', @@ -118,7 +136,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders payment request general setting and confirm its first heading', () => { - render( ); + renderWithSettingsProvider( + + ); expect( screen.queryByRole( 'heading', { @@ -128,7 +148,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders woopay breadcrumbs', () => { - render( ); + renderWithSettingsProvider( + + ); const linkToPayments = screen.getByRole( 'link', { name: 'WooPayments', @@ -140,7 +162,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders woopay settings and confirm its checkbox label', () => { - render( ); + renderWithSettingsProvider( + + ); const label = screen.getByRole( 'checkbox', { name: 'Enable WooPay', @@ -149,7 +173,9 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders WooPay express button appearance settings if feature flag is enabled and confirm its first heading', () => { - render( ); + renderWithSettingsProvider( + + ); expect( screen.queryByRole( 'heading', { @@ -161,7 +187,9 @@ describe( 'ExpressCheckoutSettings', () => { test( 'does not render WooPay express button appearance settings if feature flag is disabled', () => { global.wcpaySettings.featureFlags.woopayExpressCheckout = false; - render( ); + renderWithSettingsProvider( + + ); expect( screen.queryByRole( 'heading', { diff --git a/client/settings/express-checkout-settings/test/payment-request-button-preview.test.js b/client/settings/express-checkout-settings/__tests__/payment-request-button-preview.test.js similarity index 100% rename from client/settings/express-checkout-settings/test/payment-request-button-preview.test.js rename to client/settings/express-checkout-settings/__tests__/payment-request-button-preview.test.js diff --git a/client/settings/express-checkout-settings/test/payment-request-settings.test.js b/client/settings/express-checkout-settings/__tests__/payment-request-settings.test.js similarity index 89% rename from client/settings/express-checkout-settings/test/payment-request-settings.test.js rename to client/settings/express-checkout-settings/__tests__/payment-request-settings.test.js index 47e5a61de2b..459ebf99a42 100644 --- a/client/settings/express-checkout-settings/test/payment-request-settings.test.js +++ b/client/settings/express-checkout-settings/__tests__/payment-request-settings.test.js @@ -19,10 +19,12 @@ import { usePaymentRequestButtonTheme, useWooPayEnabledSettings, } from '../../../data'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; jest.mock( '../../../data', () => ( { usePaymentRequestEnabledSettings: jest.fn(), usePaymentRequestLocations: jest.fn(), + useTestMode: jest.fn().mockReturnValue( [ false ] ), usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'default' ] ), usePaymentRequestButtonBorderRadius: jest.fn().mockReturnValue( [ 4 ] ), usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ), @@ -63,6 +65,15 @@ const getMockPaymentRequestLocations = ( updatePaymentRequestLocationsHandler, ]; +const renderWithSettingsProvider = ( ui ) => + render( + + { ui } + + ); + describe( 'PaymentRequestSettings', () => { beforeEach( () => { usePaymentRequestEnabledSettings.mockReturnValue( @@ -79,7 +90,9 @@ describe( 'PaymentRequestSettings', () => { } ); it( 'renders enable settings with defaults', () => { - render( ); + renderWithSettingsProvider( + + ); // confirm there is a heading expect( @@ -104,7 +117,9 @@ describe( 'PaymentRequestSettings', () => { ) ); - render( ); + renderWithSettingsProvider( + + ); expect( updateIsPaymentRequestEnabledHandler ).not.toHaveBeenCalled(); @@ -119,7 +134,9 @@ describe( 'PaymentRequestSettings', () => { } ); it( 'renders general settings with defaults', () => { - render( ); + renderWithSettingsProvider( + + ); // confirm settings headings expect( @@ -163,7 +180,9 @@ describe( 'PaymentRequestSettings', () => { updatePaymentRequestLocationsHandler ) ); - render( ); + renderWithSettingsProvider( + + ); expect( updatePaymentRequestLocationsHandler ).not.toHaveBeenCalled(); @@ -201,7 +220,9 @@ describe( 'PaymentRequestSettings', () => { setButtonThemeMock, ] ); - render( ); + renderWithSettingsProvider( + + ); expect( setButtonTypeMock ).not.toHaveBeenCalled(); expect( setButtonSizeMock ).not.toHaveBeenCalled(); @@ -237,7 +258,9 @@ describe( 'PaymentRequestSettings', () => { ) ); - render( ); + renderWithSettingsProvider( + + ); // Uncheck each checkbox, and verify them what kind of action should have been called userEvent.click( screen.getByText( 'Product Page' ) ); diff --git a/client/settings/express-checkout-settings/test/woopay-settings.test.js b/client/settings/express-checkout-settings/__tests__/woopay-settings.test.js similarity index 100% rename from client/settings/express-checkout-settings/test/woopay-settings.test.js rename to client/settings/express-checkout-settings/__tests__/woopay-settings.test.js diff --git a/client/settings/express-checkout-settings/payment-request-settings.js b/client/settings/express-checkout-settings/payment-request-settings.js index a676eba6bbe..eb473f0626b 100644 --- a/client/settings/express-checkout-settings/payment-request-settings.js +++ b/client/settings/express-checkout-settings/payment-request-settings.js @@ -15,6 +15,7 @@ import { usePaymentRequestEnabledSettings, usePaymentRequestLocations, } from 'wcpay/data'; +import GooglePayTestModeCompatibilityNotice from '../google-pay-test-mode-compatibility-notice'; const PaymentRequestSettings = ( { section } ) => { const [ @@ -44,6 +45,7 @@ const PaymentRequestSettings = ( { section } ) => { { section === 'enable' && ( + { const id = 'apple_pay_google_pay'; @@ -171,6 +172,7 @@ const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => { + { isDuplicate && ( ( { + useTestMode: jest.fn().mockReturnValue( [] ), usePaymentRequestEnabledSettings: jest.fn(), useWooPayEnabledSettings: jest.fn(), useEnabledPaymentMethodIds: jest.fn(), @@ -70,7 +71,7 @@ describe( 'ExpressCheckout', () => { ) ); - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; render( @@ -84,7 +85,7 @@ describe( 'ExpressCheckout', () => { } ); it( 'has the correct href links to the express checkout settings pages', async () => { - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); @@ -112,7 +113,7 @@ describe( 'ExpressCheckout', () => { } ); it( 'hide link payment if card payment method is inactive', async () => { - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'link' ] ] ); @@ -126,7 +127,7 @@ describe( 'ExpressCheckout', () => { } ); it( 'show link payment if card payment method is active', async () => { - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); @@ -140,7 +141,7 @@ describe( 'ExpressCheckout', () => { } ); it( 'test stripe link checkbox checked', async () => { - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); @@ -154,7 +155,7 @@ describe( 'ExpressCheckout', () => { } ); it( 'test stripe link checkbox not checked', async () => { - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card' ] ] ); @@ -172,7 +173,7 @@ describe( 'ExpressCheckout', () => { useWooPayEnabledSettings.mockReturnValue( getMockWooPayEnabledSettings( false, updateIsWooPayEnabledHandler ) ); - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); @@ -200,7 +201,7 @@ describe( 'ExpressCheckout', () => { useWooPayEnabledSettings.mockReturnValue( getMockWooPayEnabledSettings( false, updateIsWooPayEnabledHandler ) ); - const context = { featureFlags: { woopay: true } }; + const context = { accountStatus: {}, featureFlags: { woopay: true } }; useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); diff --git a/client/settings/google-pay-test-mode-compatibility-notice.tsx b/client/settings/google-pay-test-mode-compatibility-notice.tsx new file mode 100644 index 00000000000..00c02220350 --- /dev/null +++ b/client/settings/google-pay-test-mode-compatibility-notice.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; +import React, { useContext } from 'react'; + +/** + * Internal dependencies + */ +import { usePaymentRequestEnabledSettings, useTestMode } from 'wcpay/data'; +import InlineNotice from 'wcpay/components/inline-notice'; +import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context'; + +const GooglePayTestModeCompatibilityNotice = () => { + const [ isTestModeEnabled ] = useTestMode(); + const [ isPaymentRequestEnabled ] = usePaymentRequestEnabledSettings(); + const { + accountStatus: { isLive: isLiveAccount }, + } = useContext( WCPaySettingsContext ); + + if ( ! isLiveAccount ) { + return null; + } + + if ( ! isTestModeEnabled ) { + return null; + } + + if ( ! isPaymentRequestEnabled ) { + return null; + } + + return ( + + { interpolateComponents( { + mixedString: __( + 'Google Pay is incompatible with test mode when using production credentials. {{learnMore}}Learn more{{/learnMore}}.', + 'woocommerce-payments' + ), + components: { + learnMore: ( + + ), + }, + } ) } + + ); +}; + +export default GooglePayTestModeCompatibilityNotice; diff --git a/client/settings/payment-methods-list/payment-method.tsx b/client/settings/payment-methods-list/payment-method.tsx index 18f978a6caa..d6611a77ed9 100644 --- a/client/settings/payment-methods-list/payment-method.tsx +++ b/client/settings/payment-methods-list/payment-method.tsx @@ -130,7 +130,7 @@ const PaymentMethod = ( { upeCapabilityStatuses.INACTIVE === status || isPoInProgress; const { accountFees, - }: { accountFees: Record< string, FeeStructure > } = useContext( + }: { accountFees?: Record< string, FeeStructure > } = useContext( WCPaySettingsContext ); const [ isManualCaptureEnabled ] = useManualCapture(); diff --git a/client/settings/wcpay-settings-context.js b/client/settings/wcpay-settings-context.js index 531e806ff27..92a92427908 100644 --- a/client/settings/wcpay-settings-context.js +++ b/client/settings/wcpay-settings-context.js @@ -3,15 +3,6 @@ */ import { createContext } from 'react'; -const WCPaySettingsContext = createContext( { - accountFees: {}, - accountLoans: {}, - accountStatus: {}, - featureFlags: { - isAuthAndCaptureEnabled: false, - isDisputeIssuerEvidenceEnabled: false, - woopay: false, - }, -} ); +const WCPaySettingsContext = createContext( window.wcpaySettings ); export default WCPaySettingsContext; diff --git a/client/tokenized-express-checkout/blocks/index.js b/client/tokenized-express-checkout/blocks/index.js index d2cefd85eb7..b8b8a6f9529 100644 --- a/client/tokenized-express-checkout/blocks/index.js +++ b/client/tokenized-express-checkout/blocks/index.js @@ -39,9 +39,7 @@ export const tokenizedExpressCheckoutElementApplePay = ( api ) => ( { return false; } - return new Promise( ( resolve ) => { - checkPaymentMethodIsAvailable( 'applePay', cart, resolve ); - } ); + return checkPaymentMethodIsAvailable( 'applePay', cart ); }, } ); @@ -77,9 +75,7 @@ export const tokenizedExpressCheckoutElementGooglePay = ( api ) => { return false; } - return new Promise( ( resolve ) => { - checkPaymentMethodIsAvailable( 'googlePay', cart, resolve ); - } ); + return checkPaymentMethodIsAvailable( 'googlePay', cart ); }, }; }; diff --git a/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js b/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js index b592169da22..5beb7e32942 100644 --- a/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js +++ b/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js @@ -14,71 +14,75 @@ import WCPayAPI from 'wcpay/checkout/api'; import { getUPEConfig } from 'wcpay/utils/checkout'; export const checkPaymentMethodIsAvailable = memoize( - ( paymentMethod, cart, resolve ) => { - // Create the DIV container on the fly - const containerEl = document.createElement( 'div' ); + ( paymentMethod, cart ) => { + return new Promise( ( resolve ) => { + // Create the DIV container on the fly + const containerEl = document.createElement( 'div' ); - // Ensure the element is hidden and doesn’t interfere with the page layout. - containerEl.style.display = 'none'; + // Ensure the element is hidden and doesn’t interfere with the page layout. + containerEl.style.display = 'none'; - document.querySelector( 'body' ).appendChild( containerEl ); + document.querySelector( 'body' ).appendChild( containerEl ); - const root = ReactDOM.createRoot( containerEl ); + const root = ReactDOM.createRoot( containerEl ); - const api = new WCPayAPI( - { - publishableKey: getUPEConfig( 'publishableKey' ), - accountId: getUPEConfig( 'accountId' ), - forceNetworkSavedCards: getUPEConfig( - 'forceNetworkSavedCards' - ), - locale: getUPEConfig( 'locale' ), - isStripeLinkEnabled: isLinkEnabled( - getUPEConfig( 'paymentMethodsConfig' ) - ), - }, - request - ); + const api = new WCPayAPI( + { + publishableKey: getUPEConfig( 'publishableKey' ), + accountId: getUPEConfig( 'accountId' ), + forceNetworkSavedCards: getUPEConfig( + 'forceNetworkSavedCards' + ), + locale: getUPEConfig( 'locale' ), + isStripeLinkEnabled: isLinkEnabled( + getUPEConfig( 'paymentMethodsConfig' ) + ), + }, + request + ); - root.render( - - resolve( false ) } + root.render( + { - let canMakePayment = false; - if ( event.availablePaymentMethods ) { - canMakePayment = - event.availablePaymentMethods[ paymentMethod ]; - } - resolve( canMakePayment ); - root.unmount(); - containerEl.remove(); - } } - /> - - ); + > + resolve( false ) } + options={ { + paymentMethods: { + amazonPay: 'never', + applePay: + paymentMethod === 'applePay' + ? 'always' + : 'never', + googlePay: + paymentMethod === 'googlePay' + ? 'always' + : 'never', + link: 'never', + paypal: 'never', + }, + } } + onReady={ ( event ) => { + let canMakePayment = false; + if ( event.availablePaymentMethods ) { + canMakePayment = + event.availablePaymentMethods[ + paymentMethod + ]; + } + resolve( canMakePayment ); + root.unmount(); + containerEl.remove(); + } } + /> + + ); + } ); } ); diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 5bbe260b5f3..7346b408e1f 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -114,7 +114,7 @@ export const getCurrentBaseFee = ( }; export const formatMethodFeesTooltip = ( - accountFees: FeeStructure + accountFees?: FeeStructure ): JSX.Element => { if ( ! accountFees ) return <>>; diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 518d189addd..d21fca9376d 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -351,6 +351,7 @@ public function get_account_status_data(): array { 'status' => $account['status'], 'created' => $account['created'] ?? '', 'testDrive' => $account['is_test_drive'] ?? false, + 'isLive' => $account['is_live'] ?? false, 'paymentsEnabled' => $account['payments_enabled'], 'detailsSubmitted' => $account['details_submitted'] ?? true, 'deposits' => $account['deposits'] ?? [], diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php index 413b51aa064..073e09b0ad9 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php @@ -307,6 +307,7 @@ public function scripts() { } wp_localize_script( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'wcpayExpressCheckoutParams', $express_checkout_params ); + wp_localize_script( 'WCPAY_BLOCKS_CHECKOUT', 'wcpayExpressCheckoutParams', $express_checkout_params ); wp_set_script_translations( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'woocommerce-payments' ); diff --git a/tests/js/jest.config.js b/tests/js/jest.config.js index 7665524e08d..f6afe00fa8d 100644 --- a/tests/js/jest.config.js +++ b/tests/js/jest.config.js @@ -45,6 +45,14 @@ module.exports = { '/docker/', '/tests/e2e', ], + watchPathIgnorePatterns: [ + '/node_modules/', + '/vendor/', + '/.*/build/', + '/.*/build-module/', + '/docker/', + '/tests/e2e', + ], transform: { ...tsjPreset.transform, '^.+\\.(jpg|svg|png|gif)(\\?.*)?$': '/tests/js/fileMock.js',