From 9eab5e468f63c06fe21b66d51347fc3231f1b8a2 Mon Sep 17 00:00:00 2001 From: Francesco Date: Fri, 17 May 2024 10:08:28 +0200 Subject: [PATCH] feat: error message on 1M+ amount (#8793) --- changelog/feat-error-notice-on-1m-amount | 4 ++ client/checkout/blocks/payment-elements.js | 71 +++++++++++++------ client/checkout/blocks/payment-processor.js | 14 ++-- client/checkout/classic/payment-processing.js | 32 ++++++++- .../classic/test/payment-processing.test.js | 22 ++++++ 5 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 changelog/feat-error-notice-on-1m-amount diff --git a/changelog/feat-error-notice-on-1m-amount b/changelog/feat-error-notice-on-1m-amount new file mode 100644 index 00000000000..dfb938d19d1 --- /dev/null +++ b/changelog/feat-error-notice-on-1m-amount @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +feat: error message on 1M+ amount diff --git a/client/checkout/blocks/payment-elements.js b/client/checkout/blocks/payment-elements.js index b687e407851..2b181d14eaa 100644 --- a/client/checkout/blocks/payment-elements.js +++ b/client/checkout/blocks/payment-elements.js @@ -1,3 +1,11 @@ +/** + * External dependencies + */ +import { useEffect, useState, RawHTML } from '@wordpress/element'; +import { Elements } from '@stripe/react-stripe-js'; +// eslint-disable-next-line import/no-unresolved +import { StoreNotice } from '@woocommerce/blocks-checkout'; + /** * Internal dependencies */ @@ -6,14 +14,16 @@ import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles'; import { getUPEConfig } from 'wcpay/utils/checkout'; import { useFingerprint } from './hooks'; import { LoadableBlock } from 'wcpay/components/loadable'; -import { Elements } from '@stripe/react-stripe-js'; -import { useEffect, useState } from 'react'; import PaymentProcessor from './payment-processor'; import { getPaymentMethodTypes } from 'wcpay/checkout/utils/upe'; const PaymentElements = ( { api, ...props } ) => { const stripe = api.getStripeForUPE( props.paymentMethodId ); const [ errorMessage, setErrorMessage ] = useState( null ); + const [ + paymentProcessorLoadErrorMessage, + setPaymentProcessorLoadErrorMessage, + ] = useState( undefined ); const [ appearance, setAppearance ] = useState( getUPEConfig( 'wcBlocksUPEAppearance' ) ); @@ -50,27 +60,42 @@ const PaymentElements = ( { api, ...props } ) => { ] ); return ( - - - - - + <> + + + { paymentProcessorLoadErrorMessage?.error?.message && ( +
+ + + { + paymentProcessorLoadErrorMessage.error + .message + } + + +
+ ) } + +
+
+ ); }; diff --git a/client/checkout/blocks/payment-processor.js b/client/checkout/blocks/payment-processor.js index 694a9064e0a..aaaf3e21772 100644 --- a/client/checkout/blocks/payment-processor.js +++ b/client/checkout/blocks/payment-processor.js @@ -49,6 +49,8 @@ const getFraudPreventionToken = () => { return window.wcpayFraudPreventionToken ?? ''; }; +const noop = () => null; + const PaymentProcessor = ( { api, activePaymentMethod, @@ -60,10 +62,11 @@ const PaymentProcessor = ( { errorMessage, shouldSavePayment, fingerprint, + onLoadError = noop, } ) => { const stripe = useStripe(); const elements = useElements(); - const isPaymentElementCompleteRef = useRef( false ); + const isPaymentInformationCompleteRef = useRef( false ); const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); const isTestMode = getUPEConfig( 'testMode' ); @@ -137,7 +140,7 @@ const PaymentProcessor = ( { return; } - if ( ! isPaymentElementCompleteRef.current ) { + if ( ! isPaymentInformationCompleteRef.current ) { return { type: 'error', message: __( @@ -234,8 +237,8 @@ const PaymentProcessor = ( { shouldSavePayment ); - const updatePaymentElementCompletionStatus = ( event ) => { - isPaymentElementCompleteRef.current = event.complete; + const setPaymentInformationCompletionStatus = ( event ) => { + isPaymentInformationCompleteRef.current = event.complete; }; return ( @@ -253,7 +256,8 @@ const PaymentProcessor = ( { shouldSavePayment, paymentMethodsConfig ) } - onChange={ updatePaymentElementCompletionStatus } + onLoadError={ onLoadError } + onChange={ setPaymentInformationCompletionStatus } className="wcpay-payment-element" /> diff --git a/client/checkout/classic/payment-processing.js b/client/checkout/classic/payment-processing.js index 704b0f1f97d..74ab31c7e36 100644 --- a/client/checkout/classic/payment-processing.js +++ b/client/checkout/classic/payment-processing.js @@ -34,6 +34,7 @@ for ( const paymentMethodType in getUPEConfig( 'paymentMethodsConfig' ) ) { gatewayUPEComponents[ paymentMethodType ] = { elements: null, upeElement: null, + isPaymentInformationComplete: false, }; } @@ -400,6 +401,27 @@ export async function mountStripePaymentElement( api, domElement ) { gatewayUPEComponents[ paymentMethodType ].upeElement || ( await createStripePaymentElement( api, paymentMethodType ) ); upeElement.mount( domElement ); + upeElement.on( 'change', ( e ) => { + gatewayUPEComponents[ paymentMethodType ].isPaymentInformationComplete = + e.complete; + } ); + upeElement.on( 'loaderror', ( e ) => { + // unset any styling to ensure the WC error message wrapper can take more width. + domElement.style.padding = '0'; + // creating a new element to be added to the DOM, so that the message can be displayed. + const messageWrapper = document.createElement( 'div' ); + messageWrapper.classList.add( 'woocommerce-error' ); + messageWrapper.innerHTML = e.error.message; + messageWrapper.style.margin = '0'; + domElement.appendChild( messageWrapper ); + // hiding any "save payment method" checkboxes. + const savePaymentMethodWrapper = domElement + .closest( '.payment_box' ) + ?.querySelector( '.woocommerce-SavedPaymentMethods-saveNew' ); + if ( savePaymentMethodWrapper ) { + savePaymentMethodWrapper.style.display = 'none'; + } + } ); } export async function mountStripePaymentMethodMessagingElement( @@ -493,9 +515,15 @@ export const processPayment = ( return; } - blockUI( $form ); + const { elements, isPaymentInformationComplete } = gatewayUPEComponents[ + paymentMethodType + ]; + if ( ! isPaymentInformationComplete ) { + showErrorCheckout( 'Your payment information is incomplete.' ); + return false; + } - const elements = gatewayUPEComponents[ paymentMethodType ].elements; + blockUI( $form ); ( async () => { try { diff --git a/client/checkout/classic/test/payment-processing.test.js b/client/checkout/classic/test/payment-processing.test.js index df5ac6ef2ca..0dd6a9ef2a4 100644 --- a/client/checkout/classic/test/payment-processing.test.js +++ b/client/checkout/classic/test/payment-processing.test.js @@ -65,10 +65,25 @@ const mockUpdateFunction = jest.fn(); const mockMountFunction = jest.fn(); +let eventHandlersFromElementsCreate = {}; const mockCreateFunction = jest.fn( () => ( { mount: mockMountFunction, update: mockUpdateFunction, + on: ( event, handler ) => { + if ( ! eventHandlersFromElementsCreate[ event ] ) { + eventHandlersFromElementsCreate[ event ] = []; + } + eventHandlersFromElementsCreate[ event ].push( handler ); + }, } ) ); +const callAllCreateHandlersWith = ( event, ...args ) => { + eventHandlersFromElementsCreate[ event ]?.forEach( ( handler ) => { + handler.apply( null, args ); + } ); +}; +const markAllPaymentElementsAsComplete = () => { + callAllCreateHandlersWith( 'change', { complete: true } ); +}; const mockSubmit = jest.fn( () => ( { then: jest.fn(), @@ -95,6 +110,7 @@ describe( 'Stripe Payment Element mounting', () => { beforeEach( () => { mockDomElement = document.createElement( 'div' ); + eventHandlersFromElementsCreate = {}; getUPEConfig.mockImplementation( ( argument ) => { if ( argument === 'wcBlocksUPEAppearance' || @@ -380,6 +396,7 @@ describe( 'Payment processing', () => { mockDomElement.dataset.paymentMethodType = 'card'; await mountStripePaymentElement( apiMock, mockDomElement ); + markAllPaymentElementsAsComplete(); const mockJqueryForm = { submit: jest.fn(), @@ -426,6 +443,7 @@ describe( 'Payment processing', () => { mockDomElement.dataset.paymentMethodType = 'card'; await mountStripePaymentElement( apiMock, mockDomElement ); + markAllPaymentElementsAsComplete(); const checkoutForm = { submit: jest.fn(), @@ -467,6 +485,7 @@ describe( 'Payment processing', () => { mockDomElement.dataset.paymentMethodType = 'card'; await mountStripePaymentElement( apiMock, mockDomElement ); + markAllPaymentElementsAsComplete(); const checkoutForm = { submit: jest.fn(), @@ -504,6 +523,7 @@ describe( 'Payment processing', () => { mockDomElement.dataset.paymentMethodType = 'card'; await mountStripePaymentElement( apiMock, mockDomElement ); + markAllPaymentElementsAsComplete(); const checkoutForm = { submit: jest.fn(), @@ -538,6 +558,7 @@ describe( 'Payment processing', () => { mockDomElement.dataset.paymentMethodType = 'card'; await mountStripePaymentElement( apiMock, mockDomElement ); + markAllPaymentElementsAsComplete(); const checkoutForm = { submit: jest.fn(), @@ -570,6 +591,7 @@ describe( 'Payment processing', () => { mockDomElement.dataset.paymentMethodType = 'card'; await mountStripePaymentElement( apiMock, mockDomElement ); + markAllPaymentElementsAsComplete(); const addPaymentMethodForm = { submit: jest.fn(),