Skip to content
Open
Show file tree
Hide file tree
Changes from all 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();
} );
} );
42 changes: 42 additions & 0 deletions client/settings/notification-settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/** @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';
import './style.scss';

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;
11 changes: 11 additions & 0 deletions client/settings/notification-settings/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.notification-settings {
.components-notice {
margin-left: 0;
margin-right: 0;
margin-bottom: 1em;
}

.settings__notifications-email-input {
max-width: 500px;
}
}
Loading
Loading