Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/dev-specify-notifications-email
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add ability to specify preferred communications email.
4 changes: 4 additions & 0 deletions client/data/settings/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ export function updateIsStripeBillingEnabled( isEnabled ) {
return updateSettingsValues( { is_stripe_billing_enabled: isEnabled } );
}

export function updateCommunicationsEmail( email ) {
return updateSettingsValues( { communications_email: email } );
}

export function* submitStripeBillingSubscriptionMigration() {
try {
yield dispatch( STORE_NAME ).startResolution(
Expand Down
16 changes: 16 additions & 0 deletions client/data/settings/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ export const usePaymentRequestButtonBorderRadius = () => {
];
};

/**
* @return {import('wcpay/types/wcpay-data-settings-hooks').SavingError | null}
*/
export const useGetSavingError = () => {
return useSelect( ( select ) => select( STORE_NAME ).getSavingError(), [] );
};
Expand Down Expand Up @@ -607,3 +610,16 @@ export const useStripeBillingMigration = () => {
];
}, [] );
};

/**
* @return {import('wcpay/types/wcpay-data-settings-hooks').GenericSettingsHook<string>}
*/
export const useCommunicationsEmail = () => {
const { updateCommunicationsEmail } = useDispatch( STORE_NAME );

const communicationsEmail = useSelect( ( select ) =>
select( STORE_NAME ).getCommunicationsEmail()
);

return [ communicationsEmail, updateCommunicationsEmail ];
};
4 changes: 4 additions & 0 deletions client/data/settings/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,7 @@ export const getStripeBillingSubscriptionCount = ( state ) => {
export const getStripeBillingMigratedCount = ( state ) => {
return getSettings( state ).stripe_billing_migrated_count || 0;
};

export const getCommunicationsEmail = ( state ) => {
return getSettings( state ).communications_email || '';
};
87 changes: 87 additions & 0 deletions client/settings/notification-settings/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/** @format */

/**
* External dependencies
*/
import React from 'react';
import { render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import NotificationSettings, {
NotificationSettingsDescription,
} from '../index';
import { useCommunicationsEmail, useGetSavingError } from 'wcpay/data';

jest.mock( 'wcpay/data', () => ( {
useCommunicationsEmail: jest.fn(),
useGetSavingError: jest.fn(),
} ) );

const mockUseCommunicationsEmail = useCommunicationsEmail as jest.MockedFunction<
typeof useCommunicationsEmail
>;
const mockUseGetSavingError = useGetSavingError as jest.MockedFunction<
typeof useGetSavingError
>;

describe( 'NotificationSettings', () => {
beforeEach( () => {
mockUseCommunicationsEmail.mockReturnValue( [
'[email protected]',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( null );
} );

it( 'renders the notification settings section', () => {
render( <NotificationSettings /> );

expect(
screen.getByLabelText( 'Communications email' )
).toBeInTheDocument();
} );

it( 'renders with the communications email input', () => {
const testEmail = '[email protected]';
mockUseCommunicationsEmail.mockReturnValue( [ testEmail, jest.fn() ] );

render( <NotificationSettings /> );

expect( screen.getByDisplayValue( testEmail ) ).toBeInTheDocument();
} );
} );

describe( 'NotificationSettingsDescription', () => {
it( 'renders the title', () => {
render( <NotificationSettingsDescription /> );

expect(
screen.getByRole( 'heading', { name: 'Notifications' } )
).toBeInTheDocument();
} );

it( 'renders the description text', () => {
render( <NotificationSettingsDescription /> );

expect(
screen.getByText(
'Configure how you receive important alerts about your WooPayments account.'
)
).toBeInTheDocument();
} );

it( 'renders the learn more link', () => {
render( <NotificationSettingsDescription /> );

const link = screen.getByRole( 'link', {
name: /Learn more/,
} );
expect( link ).toBeInTheDocument();
expect( link ).toHaveAttribute(
'href',
'https://woocommerce.com/document/woopayments/'
);
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/** @format */

/**
* External dependencies
*/
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import NotificationsEmailInput from '../notifications-email-input';
import { useGetSavingError, useCommunicationsEmail } from 'wcpay/data';

jest.mock( 'wcpay/data', () => ( {
useCommunicationsEmail: jest.fn(),
useGetSavingError: jest.fn(),
} ) );

const mockUseCommunicationsEmail = useCommunicationsEmail as jest.MockedFunction<
typeof useCommunicationsEmail
>;
const mockUseGetSavingError = useGetSavingError as jest.MockedFunction<
typeof useGetSavingError
>;

describe( 'NotificationsEmailInput', () => {
beforeEach( () => {
mockUseCommunicationsEmail.mockReturnValue( [
'[email protected]',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( null );
} );

it( 'displays and updates email address', () => {
const oldEmail = '[email protected]';
const setCommunicationsEmail = jest.fn();
mockUseCommunicationsEmail.mockReturnValue( [
oldEmail,
setCommunicationsEmail,
] );

render( <NotificationsEmailInput /> );

expect( screen.getByDisplayValue( oldEmail ) ).toBeInTheDocument();

const newEmail = '[email protected]';
fireEvent.change( screen.getByLabelText( 'Communications email' ), {
target: { value: newEmail },
} );

expect( setCommunicationsEmail ).toHaveBeenCalledWith( newEmail );
} );

it( 'displays error message for empty email', () => {
mockUseCommunicationsEmail.mockReturnValue( [ '', jest.fn() ] );
mockUseGetSavingError.mockReturnValue( {
code: 'rest_invalid_param',
message: 'Invalid parameter(s): communications_email',
data: {
status: 400,
params: {
communications_email:
'Error: Communications email is required.',
},
details: {
communications_email: {
code: 'rest_invalid_pattern',
message: 'Error: Communications email is required.',
data: null,
},
},
},
} );

const { container } = render( <NotificationsEmailInput /> );
expect(
container.querySelector( '.components-notice.is-error' )
?.textContent
).toMatch( /Error: Communications email is required./ );
} );

it( 'displays the error message for invalid email', () => {
mockUseCommunicationsEmail.mockReturnValue( [
'invalid.email',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( {
code: 'rest_invalid_param',
message: 'Invalid parameter(s): communications_email',
data: {
status: 400,
params: {
communications_email:
'Error: Invalid email address: invalid.email',
},
details: {
communications_email: {
code: 'rest_invalid_pattern',
message: 'Error: Invalid email address: invalid.email',
data: null,
},
},
},
} );

const { container } = render( <NotificationsEmailInput /> );
expect(
container.querySelector( '.components-notice.is-error' )
?.textContent
).toMatch( /Error: Invalid email address: / );
} );

it( 'does not display error when saving error is null', () => {
mockUseCommunicationsEmail.mockReturnValue( [
'[email protected]',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( null );

const { container } = render( <NotificationsEmailInput /> );
expect(
container.querySelector( '.components-notice.is-error' )
).toBeNull();
} );

it( 'renders help text', () => {
render( <NotificationsEmailInput /> );

expect(
screen.getByText(
'Email address used for WooPayments communications.'
)
).toBeInTheDocument();
} );
} );
41 changes: 41 additions & 0 deletions client/settings/notification-settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/** @format **/

/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Card, ExternalLink } from '@wordpress/components';
import React from 'react';

/**
* Internal dependencies
*/
import CardBody from '../card-body';
import NotificationsEmailInput from './notifications-email-input';

export const NotificationSettingsDescription: React.FC = () => (
<>
<h2>{ __( 'Notifications', 'woocommerce-payments' ) }</h2>
<p>
{ __(
'Configure how you receive important alerts about your WooPayments account.',
'woocommerce-payments'
) }
</p>
<ExternalLink href="https://woocommerce.com/document/woopayments/">
{ __( 'Learn more', 'woocommerce-payments' ) }
</ExternalLink>
</>
);

const NotificationSettings: React.FC = () => {
return (
<Card className="notification-settings">
<CardBody className="wcpay-card-body">
<NotificationsEmailInput />
</CardBody>
</Card>
);
};

export default NotificationSettings;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @format **/

/**
* External dependencies
*/
import { TextControl, Notice } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import React from 'react';

/**
* Internal dependencies
*/
import { useCommunicationsEmail, useGetSavingError } from 'wcpay/data';

const NotificationsEmailInput: React.FC = () => {
const [
communicationsEmail,
setCommunicationsEmail,
] = useCommunicationsEmail();

const savingError = useGetSavingError();
const communicationsEmailError =
savingError?.data?.details?.communications_email?.message;

return (
<>
{ communicationsEmailError && (
<Notice status="error" isDismissible={ false }>
<span>{ communicationsEmailError }</span>
</Notice>
) }

<TextControl
className="settings__notifications-email-input"
help={ __(
'Email address used for WooPayments communications.',
'woocommerce-payments'
) }
label={ __( 'Communications email', 'woocommerce-payments' ) }
value={ communicationsEmail }
onChange={ setCommunicationsEmail }
data-testid={ 'notifications-email-input' }
required
__nextHasNoMarginBottom
__next40pxDefaultSize
/>
</>
);
};

export default NotificationsEmailInput;
13 changes: 13 additions & 0 deletions client/settings/settings-manager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import LoadableSettingsSection from '../loadable-settings-section';
import PaymentMethodsSection from '../payment-methods-section';
import BuyNowPayLaterSection from '../buy-now-pay-later-section';
import ErrorBoundary from '../../components/error-boundary';
import NotificationSettings, {
NotificationSettingsDescription,
} from '../notification-settings';
import {
useDepositDelayDays,
useGetDuplicatedPaymentMethodIds,
Expand Down Expand Up @@ -300,6 +303,16 @@ const SettingsManager = () => {
</ErrorBoundary>
</LoadableSettingsSection>
</SettingsSection>
<SettingsSection
description={ NotificationSettingsDescription }
id="notification-settings"
>
<LoadableSettingsSection numLines={ 20 }>
<ErrorBoundary>
<NotificationSettings />
</ErrorBoundary>
</LoadableSettingsSection>
</SettingsSection>
<SaveSettingsSection disabled={ ! isTransactionInputsValid } />
<VatFormModal
isModalOpen={ isVatFormModalOpen }
Expand Down
Loading
Loading