Skip to content

Commit

Permalink
Detect multiple enabled payment methods of the same type (#8513)
Browse files Browse the repository at this point in the history
Co-authored-by: Timur Karimov <timurkarimov@timurs-macbook-pro.home>
  • Loading branch information
timur27 and Timur Karimov authored Apr 18, 2024
1 parent d3dc84b commit 3e4160e
Show file tree
Hide file tree
Showing 22 changed files with 896 additions and 40 deletions.
4 changes: 4 additions & 0 deletions changelog/add-duplicates-detection
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Detect payment methods enabled by multiple payment gateways.
71 changes: 71 additions & 0 deletions client/components/duplicate-notice/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* External dependencies
*/
import React, { useCallback } from 'react';
import InlineNotice from '../inline-notice';
import interpolateComponents from '@automattic/interpolate-components';
import { __ } from '@wordpress/i18n';
import { getAdminUrl } from 'wcpay/utils';
import { useDispatch } from '@wordpress/data';

interface DuplicateNoticeProps {
paymentMethod: string;
dismissedDuplicateNotices: string[];
setDismissedDuplicateNotices: ( notices: string[] ) => void;
}

function DuplicateNotice( {
paymentMethod,
dismissedDuplicateNotices,
setDismissedDuplicateNotices,
}: DuplicateNoticeProps ): JSX.Element | null {
const { updateOptions } = useDispatch( 'wc/admin/options' );

const handleDismiss = useCallback( () => {
const updatedNotices = [ ...dismissedDuplicateNotices, paymentMethod ];
setDismissedDuplicateNotices( updatedNotices );
updateOptions( {
wcpay_duplicate_payment_method_notices_dismissed: updatedNotices,
} );
wcpaySettings.dismissedDuplicateNotices = updatedNotices;
}, [
paymentMethod,
dismissedDuplicateNotices,
setDismissedDuplicateNotices,
updateOptions,
] );

if ( dismissedDuplicateNotices.includes( paymentMethod ) ) {
return null;
}

return (
<InlineNotice
status="warning"
icon={ true }
isDismissible={ true }
onRemove={ handleDismiss }
>
{ interpolateComponents( {
mixedString: __(
'This payment method is enabled by other extensions. {{reviewExtensions}}Review extensions{{/reviewExtensions}} to improve the shopper experience.',
'woocommerce-payments'
),
components: {
reviewExtensions: (
<a
href={ getAdminUrl( {
page: 'wc-settings',
tab: 'checkout',
} ) }
>
Review extensions
</a>
),
},
} ) }
</InlineNotice>
);
}

export default DuplicateNotice;
100 changes: 100 additions & 0 deletions client/components/duplicate-notice/tests/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* External dependencies
*/
import React from 'react';
import { render, fireEvent, screen, cleanup } from '@testing-library/react';
import { useDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import DuplicateNotice from '..';

jest.mock( '@wordpress/data', () => ( {
useDispatch: jest.fn(),
} ) );

const mockUseDispatch = useDispatch as jest.MockedFunction< any >;

describe( 'DuplicateNotice', () => {
const mockDispatch = jest.fn();
mockUseDispatch.mockReturnValue( {
updateOptions: mockDispatch,
} );

afterEach( () => {
cleanup();
} );

test( 'does not render when the payment method is dismissed', () => {
render(
<DuplicateNotice
paymentMethod="bancontact"
dismissedDuplicateNotices={ [ 'bancontact' ] }
setDismissedDuplicateNotices={ jest.fn() }
/>
);
expect(
screen.queryByText(
'This payment method is enabled by other extensions. Review extensions to improve the shopper experience.'
)
).not.toBeInTheDocument();
} );

test( 'renders correctly when the payment method is not dismissed', () => {
render(
<DuplicateNotice
paymentMethod="card"
dismissedDuplicateNotices={ [] }
setDismissedDuplicateNotices={ jest.fn() }
/>
);
expect(
screen.getByText(
'This payment method is enabled by other extensions. Review extensions to improve the shopper experience.'
)
).toBeInTheDocument();
cleanup();
} );

test( 'dismissal process triggers appropriate actions', () => {
const paymentMethod = 'ideal';
const props = {
paymentMethod: paymentMethod,
dismissedDuplicateNotices: [],
setDismissedDuplicateNotices: jest.fn(),
};
const { container } = render( <DuplicateNotice { ...props } /> );
const dismissButton = container.querySelector(
'.components-button.components-notice__dismiss.has-icon'
);
if ( dismissButton ) {
fireEvent.click( dismissButton );
} else {
throw new Error( 'Dismiss button not found' );
}

// Check if local state update function and Redux action dispatcher are called correctly
expect( props.setDismissedDuplicateNotices ).toHaveBeenCalledWith( [
paymentMethod,
] );
expect( mockDispatch ).toHaveBeenCalledWith( {
wcpay_duplicate_payment_method_notices_dismissed: [ paymentMethod ],
} );
} );

test( 'clicking on the Review extensions link navigates correctly', () => {
const { getByText } = render(
<DuplicateNotice
{ ...{
paymentMethod: 'ideal',
dismissedDuplicateNotices: [],
setDismissedDuplicateNotices: jest.fn(),
} }
/>
);
expect(
getByText( 'Review extensions' ).closest( 'a' )
).toHaveAttribute( 'href', 'admin.php?page=wc-settings&tab=checkout' );
} );
} );
17 changes: 17 additions & 0 deletions client/components/payment-methods-list/payment-method.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { getDocumentationUrlForDisabledPaymentMethod } from '../payment-method-d
import Pill from '../pill';
import InlineNotice from '../inline-notice';
import './payment-method.scss';
import DuplicateNotice from '../duplicate-notice';
import DuplicatedPaymentMethodsContext from 'wcpay/settings/settings-manager/duplicated-payment-methods-context';

interface PaymentMethodProps {
id: string;
Expand Down Expand Up @@ -144,6 +146,12 @@ const PaymentMethod = ( {
isPoInProgress ||
upeCapabilityStatuses.REJECTED === status;
const shouldDisplayNotice = id === 'sofort';
const {
duplicates,
dismissedDuplicateNotices,
setDismissedDuplicateNotices,
} = useContext( DuplicatedPaymentMethodsContext );
const isDuplicate = duplicates.includes( id );

const needsOverlay =
( isManualCaptureEnabled && ! isAllowingManualCapture ) ||
Expand Down Expand Up @@ -357,6 +365,15 @@ const PaymentMethod = ( {
</span>
</InlineNotice>
) }
{ isDuplicate && (
<DuplicateNotice
paymentMethod={ id }
dismissedDuplicateNotices={ dismissedDuplicateNotices }
setDismissedDuplicateNotices={
setDismissedDuplicateNotices
}
/>
) }
</li>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { act } from 'react-dom/test-utils';
* Internal dependencies
*/
import PaymentMethod from '../payment-method';
import DuplicatedPaymentMethodsContext from 'wcpay/settings/settings-manager/duplicated-payment-methods-context';

describe( 'PaymentMethod', () => {
let checked = false;
Expand All @@ -21,6 +22,7 @@ describe( 'PaymentMethod', () => {
const handleOnUnCheckClickMock = jest.fn( () => {
checked = false;
} );
const setDismissedDuplicateNoticesMock = jest.fn();

// Clear the mocks (including the mock call count) after each test.
afterEach( () => {
Expand Down Expand Up @@ -127,4 +129,62 @@ describe( 'PaymentMethod', () => {
expect( handleOnUnCheckClickMock ).not.toHaveBeenCalled();
jest.useRealTimers();
} );

const getDuplicateComponent = ( id: string ) => (
<PaymentMethod
label="Test Method"
id={ id }
checked={ false }
onCheckClick={ handleOnCheckClickMock }
onUncheckClick={ handleOnUnCheckClickMock }
description="Test Description"
Icon={ () => null }
status=""
isAllowingManualCapture={ false }
required={ false }
locked={ false }
isPoEnabled={ false }
isPoComplete={ false }
/>
);

test( 'does not render DuplicateNotice if payment method is not in duplicates', () => {
render(
<DuplicatedPaymentMethodsContext.Provider
value={ {
duplicates: [ 'ideal' ],
dismissedDuplicateNotices: [],
setDismissedDuplicateNotices: setDismissedDuplicateNoticesMock,
} }
>
{ getDuplicateComponent( 'card' ) }
</DuplicatedPaymentMethodsContext.Provider>
);

expect(
screen.queryByText(
'This payment method is enabled by other extensions. Review extensions to improve the shopper experience.'
)
).not.toBeInTheDocument();
} );

test( 'render DuplicateNotice if payment method is in duplicates', () => {
render(
<DuplicatedPaymentMethodsContext.Provider
value={ {
duplicates: [ 'card' ],
dismissedDuplicateNotices: [],
setDismissedDuplicateNotices: setDismissedDuplicateNoticesMock,
} }
>
{ getDuplicateComponent( 'card' ) }
</DuplicatedPaymentMethodsContext.Provider>
);

expect(
screen.queryByText(
'This payment method is enabled by other extensions. Review extensions to improve the shopper experience.'
)
).toBeInTheDocument();
} );
} );
5 changes: 5 additions & 0 deletions client/data/settings/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ export const useGetAvailablePaymentMethodIds = () =>
export const useGetPaymentMethodStatuses = () =>
useSelect( ( select ) => select( STORE_NAME ).getPaymentMethodStatuses() );

export const useGetDuplicatedPaymentMethodIds = () =>
useSelect( ( select ) =>
select( STORE_NAME ).getDuplicatedPaymentMethodIds()
);

export const useGetSettings = () =>
useSelect( ( select ) => select( STORE_NAME ).getSettings() );

Expand Down
4 changes: 4 additions & 0 deletions client/data/settings/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const getSupportAddressState = ( state ) => {
return getSettings( state ).account_business_support_address || EMPTY_OBJ;
};

export const getDuplicatedPaymentMethodIds = ( state ) => {
return getSettings( state ).duplicated_payment_method_ids || EMPTY_OBJ;
};

export const getIsWCPayEnabled = ( state ) => {
return getSettings( state ).is_wcpay_enabled || false;
};
Expand Down
29 changes: 29 additions & 0 deletions client/data/settings/test/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
useWooPayCustomMessage,
useWooPayStoreLogo,
useClientSecretEncryption,
useGetDuplicatedPaymentMethodIds,
} from '../hooks';
import { STORE_NAME } from '../../constants';

Expand Down Expand Up @@ -371,4 +372,32 @@ describe( 'Settings hooks tests', () => {
);
} );
} );

describe( 'useGetDuplicatedPaymentMethodIds', () => {
beforeEach( () => {
useSelect.mockImplementation( ( selector ) =>
selector( ( name ) => {
if ( name === STORE_NAME ) {
return {
getDuplicatedPaymentMethodIds: jest.fn( () => [
'card',
'bancontact',
] ),
};
}
return {};
} )
);
} );

test( 'returns duplicated payment method IDs from selector', () => {
const duplicatedPaymentMethodIds = useGetDuplicatedPaymentMethodIds();
expect( duplicatedPaymentMethodIds ).toEqual( [
'card',
'bancontact',
] );

expect( useSelect ).toHaveBeenCalled();
} );
} );
} );
Loading

0 comments on commit 3e4160e

Please sign in to comment.