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 updateAccountCommunicationsEmail( email ) {
return updateSettingsValues( { account_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 useAccountCommunicationsEmail = () => {
const { updateAccountCommunicationsEmail } = useDispatch( STORE_NAME );

const accountCommunicationsEmail = useSelect( ( select ) =>
select( STORE_NAME ).getAccountCommunicationsEmail()
);

return [ accountCommunicationsEmail, updateAccountCommunicationsEmail ];
};
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 getAccountCommunicationsEmail = ( state ) => {
return getSettings( state ).account_communications_email || '';
};
90 changes: 90 additions & 0 deletions client/settings/notification-settings/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/** @format */

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

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

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

const mockUseAccountCommunicationsEmail = useAccountCommunicationsEmail as jest.MockedFunction<
typeof useAccountCommunicationsEmail
>;
const mockUseGetSavingError = useGetSavingError as jest.MockedFunction<
typeof useGetSavingError
>;

describe( 'NotificationSettings', () => {
beforeEach( () => {
mockUseAccountCommunicationsEmail.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]';
mockUseAccountCommunicationsEmail.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/account-management/change-email-for-woopayments-alerts/'
);
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/** @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, useAccountCommunicationsEmail } from 'wcpay/data';

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

const mockUseAccountCommunicationsEmail = useAccountCommunicationsEmail as jest.MockedFunction<
typeof useAccountCommunicationsEmail
>;
const mockUseGetSavingError = useGetSavingError as jest.MockedFunction<
typeof useGetSavingError
>;

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

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

render( <NotificationsEmailInput /> );

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

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

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

it( 'displays error message for empty email', () => {
mockUseAccountCommunicationsEmail.mockReturnValue( [ '', jest.fn() ] );
mockUseGetSavingError.mockReturnValue( {
code: 'rest_invalid_param',
message: 'Invalid parameter(s): account_communications_email',
data: {
status: 400,
params: {
account_communications_email:
'Error: Communications email is required.',
},
details: {
account_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', () => {
mockUseAccountCommunicationsEmail.mockReturnValue( [
'invalid.email',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( {
code: 'rest_invalid_param',
message: 'Invalid parameter(s): account_communications_email',
data: {
status: 400,
params: {
account_communications_email:
'Error: Invalid email address: invalid.email',
},
details: {
account_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', () => {
mockUseAccountCommunicationsEmail.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();
} );

it( 'displays client-side validation error for invalid email after blur', () => {
mockUseAccountCommunicationsEmail.mockReturnValue( [
'invalid-email',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( null );

const { container } = render( <NotificationsEmailInput /> );

// Error should not be shown before blur
expect(
container.querySelector( '.components-notice.is-error' )
).toBeNull();

// Trigger blur event
fireEvent.blur( screen.getByLabelText( 'Communications email' ) );

// Error should be shown after blur
expect(
container.querySelector( '.components-notice.is-error' )
?.textContent
).toMatch( /Please enter a valid email address./ );
} );

it( 'does not display client-side validation error for valid email after blur', () => {
mockUseAccountCommunicationsEmail.mockReturnValue( [
'[email protected]',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( null );

const { container } = render( <NotificationsEmailInput /> );

// Trigger blur event
fireEvent.blur( screen.getByLabelText( 'Communications email' ) );

// No error should be shown for valid email
expect(
container.querySelector( '.components-notice.is-error' )
).toBeNull();
} );

it( 'server error takes precedence over client-side validation error', () => {
mockUseAccountCommunicationsEmail.mockReturnValue( [
'invalid-email',
jest.fn(),
] );
mockUseGetSavingError.mockReturnValue( {
code: 'rest_invalid_param',
message: 'Invalid parameter(s): account_communications_email',
data: {
status: 400,
params: {
account_communications_email:
'Error: Invalid email address: invalid-email',
},
details: {
account_communications_email: {
code: 'rest_invalid_pattern',
message: 'Error: Invalid email address: invalid-email',
data: null,
},
},
},
} );

const { container } = render( <NotificationsEmailInput /> );

// Trigger blur to enable client-side validation
fireEvent.blur( screen.getByLabelText( 'Communications email' ) );

// Server error should be shown instead of client-side error
expect(
container.querySelector( '.components-notice.is-error' )
?.textContent
).toMatch( /Error: Invalid email address: invalid-email/ );
} );
} );
Loading
Loading