Compare commits
1 Commits
split-full
...
djoy/use_h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
261769cca9 |
@@ -51,7 +51,7 @@ export class ConfirmationModal extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
status,
|
open,
|
||||||
errorType,
|
errorType,
|
||||||
intl,
|
intl,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -59,7 +59,6 @@ export class ConfirmationModal extends Component {
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
password,
|
password,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const open = ['confirming', 'pending', 'failed'].includes(status);
|
|
||||||
const passwordFieldId = 'passwordFieldId';
|
const passwordFieldId = 'passwordFieldId';
|
||||||
const invalidMessage = messages[this.getShortErrorMessageId(errorType)];
|
const invalidMessage = messages[this.getShortErrorMessageId(errorType)];
|
||||||
return (
|
return (
|
||||||
@@ -113,7 +112,7 @@ export class ConfirmationModal extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ConfirmationModal.propTypes = {
|
ConfirmationModal.propTypes = {
|
||||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
open: PropTypes.bool,
|
||||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
@@ -123,7 +122,7 @@ ConfirmationModal.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ConfirmationModal.defaultProps = {
|
ConfirmationModal.defaultProps = {
|
||||||
status: null,
|
open: false,
|
||||||
errorType: null,
|
errorType: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('ConfirmationModal', () => {
|
|||||||
onCancel: jest.fn(),
|
onCancel: jest.fn(),
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
onSubmit: jest.fn(),
|
onSubmit: jest.fn(),
|
||||||
status: null,
|
open: false,
|
||||||
errorType: null,
|
errorType: null,
|
||||||
password: 'fluffy bunnies',
|
password: 'fluffy bunnies',
|
||||||
logoutUrl: 'http://localhost/logout',
|
logoutUrl: 'http://localhost/logout',
|
||||||
@@ -44,7 +44,8 @@ describe('ConfirmationModal', () => {
|
|||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<IntlConfirmationModal
|
<IntlConfirmationModal
|
||||||
{...props}
|
{...props}
|
||||||
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
// This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||||
|
open
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
))
|
))
|
||||||
@@ -59,7 +60,8 @@ describe('ConfirmationModal', () => {
|
|||||||
<IntlConfirmationModal
|
<IntlConfirmationModal
|
||||||
{...props}
|
{...props}
|
||||||
errorType="empty-password"
|
errorType="empty-password"
|
||||||
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
// This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||||
|
open
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||||
import { Button, Hyperlink } from '@edx/paragon';
|
import { Button, Hyperlink } from '@edx/paragon';
|
||||||
|
|
||||||
// Actions
|
|
||||||
import {
|
|
||||||
deleteAccount,
|
|
||||||
deleteAccountConfirmation,
|
|
||||||
deleteAccountFailure,
|
|
||||||
deleteAccountReset,
|
|
||||||
deleteAccountCancel,
|
|
||||||
} from './data/actions';
|
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -21,106 +11,116 @@ import ConnectedConfirmationModal from './ConfirmationModal';
|
|||||||
import PrintingInstructions from './PrintingInstructions';
|
import PrintingInstructions from './PrintingInstructions';
|
||||||
import ConnectedSuccessModal from './SuccessModal';
|
import ConnectedSuccessModal from './SuccessModal';
|
||||||
import BeforeProceedingBanner from './BeforeProceedingBanner';
|
import BeforeProceedingBanner from './BeforeProceedingBanner';
|
||||||
|
import { postDeleteAccount } from './data/service';
|
||||||
|
import useAction from '../../common/hooks';
|
||||||
|
|
||||||
export class DeleteAccount extends React.Component {
|
function DeleteAccount(props) {
|
||||||
state = {
|
const {
|
||||||
password: '',
|
logoutUrl, hasLinkedTPA, isVerifiedAccount, intl,
|
||||||
};
|
} = props;
|
||||||
|
|
||||||
handleSubmit = () => {
|
const [password, setPassword] = useState('');
|
||||||
if (this.state.password === '') {
|
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||||
this.props.deleteAccountFailure('empty-password');
|
|
||||||
} else {
|
const [validationError, setValidationError] = useState(null);
|
||||||
this.props.deleteAccount(this.state.password);
|
const [
|
||||||
|
deleteAccountState,
|
||||||
|
performDeleteAccount,
|
||||||
|
resetDeleteAccount,
|
||||||
|
] = useAction(postDeleteAccount);
|
||||||
|
|
||||||
|
const canDelete = isVerifiedAccount && !hasLinkedTPA;
|
||||||
|
const successDialogOpen = deleteAccountState.loaded;
|
||||||
|
const error = deleteAccountState.error !== null ? 'server' : validationError;
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
const handleDeleteAccountClick = useCallback(() => {
|
||||||
|
if (canDelete) {
|
||||||
|
setConfirmationDialogOpen(true);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
handleCancel = () => {
|
const handleSubmit = useCallback(() => {
|
||||||
this.setState({ password: '' });
|
if (password === '') {
|
||||||
this.props.deleteAccountCancel();
|
setValidationError('empty-password');
|
||||||
};
|
} else {
|
||||||
|
performDeleteAccount(password);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
handlePasswordChange = (e) => {
|
const handleCancel = useCallback(() => {
|
||||||
this.setState({ password: e.target.value.trim() });
|
setPassword('');
|
||||||
this.props.deleteAccountReset();
|
setConfirmationDialogOpen(false);
|
||||||
};
|
resetDeleteAccount();
|
||||||
|
});
|
||||||
|
|
||||||
handleFinalClose = () => {
|
const handlePasswordChange = useCallback((e) => {
|
||||||
global.location = this.props.logoutUrl;
|
setPassword(e.target.value.trim());
|
||||||
};
|
setValidationError(null);
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
const handleFinalClose = useCallback(() => {
|
||||||
const {
|
global.location = logoutUrl;
|
||||||
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
|
});
|
||||||
} = this.props;
|
|
||||||
const canDelete = isVerifiedAccount && !hasLinkedTPA;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="section-heading">
|
<h2 className="section-heading">
|
||||||
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
||||||
</h2>
|
</h2>
|
||||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||||
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
|
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
|
||||||
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
|
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
|
||||||
<p>
|
<p>
|
||||||
<PrintingInstructions />
|
<PrintingInstructions />
|
||||||
</p>
|
</p>
|
||||||
<p className="text-danger h6">
|
<p className="text-danger h6">
|
||||||
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
|
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
||||||
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<Button
|
<Button
|
||||||
className="btn-outline-danger"
|
className="btn-outline-danger"
|
||||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
onClick={handleDeleteAccountClick}
|
||||||
disabled={!canDelete}
|
disabled={!canDelete}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isVerifiedAccount ? null : (
|
{isVerifiedAccount ? null : (
|
||||||
<BeforeProceedingBanner
|
<BeforeProceedingBanner
|
||||||
instructionMessageId="account.settings.delete.account.please.activate"
|
instructionMessageId="account.settings.delete.account.please.activate"
|
||||||
supportUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
supportUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasLinkedTPA ? (
|
|
||||||
<BeforeProceedingBanner
|
|
||||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
|
||||||
supportUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<ConnectedConfirmationModal
|
|
||||||
status={status}
|
|
||||||
errorType={errorType}
|
|
||||||
onSubmit={this.handleSubmit}
|
|
||||||
onCancel={this.handleCancel}
|
|
||||||
onChange={this.handlePasswordChange}
|
|
||||||
password={this.state.password}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
|
{hasLinkedTPA ? (
|
||||||
</div>
|
<BeforeProceedingBanner
|
||||||
);
|
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||||
}
|
supportUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<ConnectedConfirmationModal
|
||||||
|
open={confirmationDialogOpen}
|
||||||
|
errorType={error}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
password={password}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConnectedSuccessModal open={successDialogOpen} onClose={handleFinalClose} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteAccount.propTypes = {
|
DeleteAccount.propTypes = {
|
||||||
deleteAccount: PropTypes.func.isRequired,
|
|
||||||
deleteAccountConfirmation: PropTypes.func.isRequired,
|
|
||||||
deleteAccountFailure: PropTypes.func.isRequired,
|
|
||||||
deleteAccountReset: PropTypes.func.isRequired,
|
|
||||||
deleteAccountCancel: PropTypes.func.isRequired,
|
|
||||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
|
||||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
|
||||||
hasLinkedTPA: PropTypes.bool,
|
hasLinkedTPA: PropTypes.bool,
|
||||||
isVerifiedAccount: PropTypes.bool,
|
isVerifiedAccount: PropTypes.bool,
|
||||||
logoutUrl: PropTypes.string.isRequired,
|
logoutUrl: PropTypes.string.isRequired,
|
||||||
@@ -130,20 +130,6 @@ DeleteAccount.propTypes = {
|
|||||||
DeleteAccount.defaultProps = {
|
DeleteAccount.defaultProps = {
|
||||||
hasLinkedTPA: false,
|
hasLinkedTPA: false,
|
||||||
isVerifiedAccount: true,
|
isVerifiedAccount: true,
|
||||||
status: null,
|
|
||||||
errorType: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assume we're part of the accountSettings state.
|
export default injectIntl(DeleteAccount);
|
||||||
const mapStateToProps = state => state.accountSettings.deleteAccount;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
{
|
|
||||||
deleteAccount,
|
|
||||||
deleteAccountConfirmation,
|
|
||||||
deleteAccountFailure,
|
|
||||||
deleteAccountReset,
|
|
||||||
deleteAccountCancel,
|
|
||||||
},
|
|
||||||
)(injectIntl(DeleteAccount));
|
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { IntlProvider, injectIntl } from '@edx/frontend-i18n';
|
import { IntlProvider } from '@edx/frontend-i18n';
|
||||||
|
|
||||||
// Testing the modals separately, they just clutter up the snapshots if included here.
|
// Testing the modals separately, they just clutter up the snapshots if included here.
|
||||||
jest.mock('./ConfirmationModal');
|
jest.mock('./ConfirmationModal');
|
||||||
jest.mock('./SuccessModal');
|
jest.mock('./SuccessModal');
|
||||||
|
|
||||||
import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first
|
import DeleteAccount from './DeleteAccount'; // eslint-disable-line import/first
|
||||||
|
|
||||||
const IntlDeleteAccount = injectIntl(DeleteAccount);
|
|
||||||
|
|
||||||
describe('DeleteAccount', () => {
|
describe('DeleteAccount', () => {
|
||||||
let props = {};
|
let props = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
deleteAccount: jest.fn(),
|
|
||||||
deleteAccountConfirmation: jest.fn(),
|
|
||||||
deleteAccountFailure: jest.fn(),
|
|
||||||
deleteAccountReset: jest.fn(),
|
|
||||||
deleteAccountCancel: jest.fn(),
|
|
||||||
status: null,
|
|
||||||
errorType: null,
|
|
||||||
hasLinkedTPA: false,
|
hasLinkedTPA: false,
|
||||||
isVerifiedAccount: true,
|
isVerifiedAccount: true,
|
||||||
logoutUrl: 'http://localhost/logout',
|
logoutUrl: 'http://localhost/logout',
|
||||||
@@ -32,7 +23,7 @@ describe('DeleteAccount', () => {
|
|||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create((
|
.create((
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<IntlDeleteAccount
|
<DeleteAccount
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
@@ -45,7 +36,7 @@ describe('DeleteAccount', () => {
|
|||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create((
|
.create((
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<IntlDeleteAccount
|
<DeleteAccount
|
||||||
{...props}
|
{...props}
|
||||||
isVerifiedAccount={false}
|
isVerifiedAccount={false}
|
||||||
/>
|
/>
|
||||||
@@ -59,7 +50,7 @@ describe('DeleteAccount', () => {
|
|||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create((
|
.create((
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<IntlDeleteAccount
|
<DeleteAccount
|
||||||
{...props}
|
{...props}
|
||||||
hasLinkedTPA
|
hasLinkedTPA
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { Modal } from '@edx/paragon';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const SuccessModal = (props) => {
|
export const SuccessModal = (props) => {
|
||||||
const { status, intl, onClose } = props;
|
const { open, intl, onClose } = props;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={status === 'deleted'}
|
open={open}
|
||||||
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
|
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
|
||||||
body={
|
body={
|
||||||
<div>
|
<div>
|
||||||
@@ -26,13 +26,13 @@ export const SuccessModal = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SuccessModal.propTypes = {
|
SuccessModal.propTypes = {
|
||||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
open: PropTypes.bool,
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
SuccessModal.defaultProps = {
|
SuccessModal.defaultProps = {
|
||||||
status: null,
|
open: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SuccessModal);
|
export default injectIntl(SuccessModal);
|
||||||
|
|||||||
@@ -16,30 +16,15 @@ describe('SuccessModal', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
onClose: jest.fn(),
|
onClose: jest.fn(),
|
||||||
status: null,
|
open: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match default closed success modal snapshot', () => {
|
it('should match default closed success modal snapshot', () => {
|
||||||
let tree = renderer.create((
|
const tree = renderer.create((
|
||||||
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
|
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
|
||||||
.toJSON();
|
.toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
|
|
||||||
tree = renderer.create((
|
|
||||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>))
|
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
|
|
||||||
tree = renderer.create((
|
|
||||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>))
|
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
|
|
||||||
tree = renderer.create((
|
|
||||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>))
|
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match open success modal snapshot', () => {
|
it('should match open success modal snapshot', () => {
|
||||||
@@ -48,7 +33,8 @@ describe('SuccessModal', () => {
|
|||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<IntlSuccessModal
|
<IntlSuccessModal
|
||||||
{...props}
|
{...props}
|
||||||
status="deleted" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
// This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||||
|
open
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -61,21 +61,21 @@ exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="fade"
|
className="modal-backdrop show"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="modal js-close-modal-on-click fade"
|
className="modal js-close-modal-on-click show d-block"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-labelledby="id4"
|
aria-labelledby="id4"
|
||||||
aria-modal={true}
|
aria-modal={true}
|
||||||
className=""
|
className="modal-dialog"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
>
|
>
|
||||||
@@ -121,186 +121,3 @@ exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="fade"
|
|
||||||
role="presentation"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="modal js-close-modal-on-click fade"
|
|
||||||
onClick={[Function]}
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-labelledby="id5"
|
|
||||||
aria-modal={true}
|
|
||||||
className=""
|
|
||||||
role="dialog"
|
|
||||||
tabIndex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-content"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-header"
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className="modal-title"
|
|
||||||
id="id5"
|
|
||||||
>
|
|
||||||
We're sorry to see you go! Your account will be deleted shortly.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="modal-body"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className="h6"
|
|
||||||
>
|
|
||||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="modal-footer"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn js-close-modal-on-click btn-secondary"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="fade"
|
|
||||||
role="presentation"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="modal js-close-modal-on-click fade"
|
|
||||||
onClick={[Function]}
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-labelledby="id6"
|
|
||||||
aria-modal={true}
|
|
||||||
className=""
|
|
||||||
role="dialog"
|
|
||||||
tabIndex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-content"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-header"
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className="modal-title"
|
|
||||||
id="id6"
|
|
||||||
>
|
|
||||||
We're sorry to see you go! Your account will be deleted shortly.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="modal-body"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className="h6"
|
|
||||||
>
|
|
||||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="modal-footer"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn js-close-modal-on-click btn-secondary"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="modal-backdrop show"
|
|
||||||
role="presentation"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="modal js-close-modal-on-click show d-block"
|
|
||||||
onClick={[Function]}
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-labelledby="id7"
|
|
||||||
aria-modal={true}
|
|
||||||
className="modal-dialog"
|
|
||||||
role="dialog"
|
|
||||||
tabIndex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-content"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-header"
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className="modal-title"
|
|
||||||
id="id7"
|
|
||||||
>
|
|
||||||
We're sorry to see you go! Your account will be deleted shortly.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="modal-body"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className="h6"
|
|
||||||
>
|
|
||||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="modal-footer"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn js-close-modal-on-click btn-secondary"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { utils } from '../../../common';
|
|
||||||
|
|
||||||
const { AsyncActionType } = utils;
|
|
||||||
|
|
||||||
export const DELETE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'DELETE_ACCOUNT');
|
|
||||||
DELETE_ACCOUNT.CONFIRMATION = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CONFIRMATION';
|
|
||||||
DELETE_ACCOUNT.CANCEL = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CANCEL';
|
|
||||||
|
|
||||||
export const deleteAccount = password => ({
|
|
||||||
type: DELETE_ACCOUNT.BASE,
|
|
||||||
payload: { password },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteAccountConfirmation = () => ({
|
|
||||||
type: DELETE_ACCOUNT.CONFIRMATION,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteAccountBegin = () => ({
|
|
||||||
type: DELETE_ACCOUNT.BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteAccountSuccess = () => ({
|
|
||||||
type: DELETE_ACCOUNT.SUCCESS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteAccountFailure = reason => ({
|
|
||||||
type: DELETE_ACCOUNT.FAILURE,
|
|
||||||
payload: { reason },
|
|
||||||
});
|
|
||||||
|
|
||||||
// to clear errors from the confirmation modal
|
|
||||||
export const deleteAccountReset = () => ({
|
|
||||||
type: DELETE_ACCOUNT.RESET,
|
|
||||||
});
|
|
||||||
|
|
||||||
// to close the modal
|
|
||||||
export const deleteAccountCancel = () => ({
|
|
||||||
type: DELETE_ACCOUNT.CANCEL,
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { DELETE_ACCOUNT } from './actions';
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
status: null,
|
|
||||||
errorType: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const reducer = (state = defaultState, action = null) => {
|
|
||||||
if (action !== null) {
|
|
||||||
switch (action.type) {
|
|
||||||
case DELETE_ACCOUNT.CONFIRMATION:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
status: 'confirming',
|
|
||||||
};
|
|
||||||
|
|
||||||
case DELETE_ACCOUNT.BEGIN:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
status: 'pending',
|
|
||||||
};
|
|
||||||
|
|
||||||
case DELETE_ACCOUNT.SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
status: 'deleted',
|
|
||||||
};
|
|
||||||
|
|
||||||
case DELETE_ACCOUNT.FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
status: 'failed',
|
|
||||||
errorType: action.payload.reason || 'server',
|
|
||||||
};
|
|
||||||
|
|
||||||
case DELETE_ACCOUNT.RESET: {
|
|
||||||
const oldStatus = state.status;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
// clear the error state if applicable, otherwise don't change state
|
|
||||||
status: oldStatus === 'failed' ? 'confirming' : oldStatus,
|
|
||||||
errorType: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case DELETE_ACCOUNT.CANCEL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
status: null,
|
|
||||||
errorType: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reducer;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import reducer from './reducers';
|
|
||||||
import {
|
|
||||||
deleteAccountConfirmation,
|
|
||||||
deleteAccountBegin,
|
|
||||||
deleteAccountSuccess,
|
|
||||||
deleteAccountFailure,
|
|
||||||
deleteAccountReset,
|
|
||||||
deleteAccountCancel,
|
|
||||||
} from './actions';
|
|
||||||
|
|
||||||
describe('delete-account reducer', () => {
|
|
||||||
let state = null;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
state = reducer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.CONFIRMATION', () => {
|
|
||||||
const result = reducer(state, deleteAccountConfirmation());
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: 'confirming',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.BEGIN', () => {
|
|
||||||
const result = reducer(state, deleteAccountBegin());
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: 'pending',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.SUCCESS', () => {
|
|
||||||
const result = reducer(state, deleteAccountSuccess());
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: 'deleted',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.FAILURE no reason', () => {
|
|
||||||
const result = reducer(state, deleteAccountFailure());
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: 'server',
|
|
||||||
status: 'failed',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.FAILURE with reason', () => {
|
|
||||||
const result = reducer(state, deleteAccountFailure('carnivorous buns'));
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: 'carnivorous buns',
|
|
||||||
status: 'failed',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.RESET no status', () => {
|
|
||||||
const result = reducer(state, deleteAccountReset());
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.RESET with failed old status', () => {
|
|
||||||
const result = reducer(
|
|
||||||
{
|
|
||||||
errorType: 'carnivorous buns',
|
|
||||||
status: 'failed',
|
|
||||||
},
|
|
||||||
deleteAccountReset(),
|
|
||||||
);
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: 'confirming',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.RESET with pending old status', () => {
|
|
||||||
const result = reducer(
|
|
||||||
{
|
|
||||||
errorType: 'carnivorous buns',
|
|
||||||
status: 'pending',
|
|
||||||
},
|
|
||||||
deleteAccountReset(),
|
|
||||||
);
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: 'pending',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process DELETE_ACCOUNT.CANCEL', () => {
|
|
||||||
const result = reducer(
|
|
||||||
{
|
|
||||||
errorType: 'carnivorous buns',
|
|
||||||
status: 'failed',
|
|
||||||
},
|
|
||||||
deleteAccountCancel(),
|
|
||||||
);
|
|
||||||
expect(result).toEqual({
|
|
||||||
errorType: null,
|
|
||||||
status: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { put, call, takeEvery } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DELETE_ACCOUNT,
|
|
||||||
deleteAccountBegin,
|
|
||||||
deleteAccountSuccess,
|
|
||||||
deleteAccountFailure,
|
|
||||||
} from './actions';
|
|
||||||
|
|
||||||
import { postDeleteAccount } from './service';
|
|
||||||
|
|
||||||
export function* handleDeleteAccount(action) {
|
|
||||||
try {
|
|
||||||
yield put(deleteAccountBegin());
|
|
||||||
const response = yield call(postDeleteAccount, action.payload.password);
|
|
||||||
yield put(deleteAccountSuccess(response));
|
|
||||||
} catch (e) {
|
|
||||||
if (typeof e.response.data === 'string') {
|
|
||||||
yield put(deleteAccountFailure());
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function* saga() {
|
|
||||||
yield takeEvery(DELETE_ACCOUNT.BASE, handleDeleteAccount);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,2 @@
|
|||||||
export { default } from './DeleteAccount';
|
export { default } from './DeleteAccount';
|
||||||
export { default as reducer } from './data/reducers';
|
|
||||||
export { default as saga } from './data/sagas';
|
|
||||||
export { configureService } from './data/service';
|
export { configureService } from './data/service';
|
||||||
export { DELETE_ACCOUNT } from './data/actions';
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
RESET_DRAFTS,
|
RESET_DRAFTS,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from './delete-account';
|
|
||||||
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from './site-language';
|
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from './site-language';
|
||||||
import { reducer as resetPasswordReducer, RESET_PASSWORD } from './reset-password';
|
import { reducer as resetPasswordReducer, RESET_PASSWORD } from './reset-password';
|
||||||
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from './third-party-auth';
|
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from './third-party-auth';
|
||||||
@@ -27,7 +26,6 @@ export const defaultState = {
|
|||||||
timeZones: [],
|
timeZones: [],
|
||||||
countryTimeZones: [],
|
countryTimeZones: [],
|
||||||
previousSiteLanguage: null,
|
previousSiteLanguage: null,
|
||||||
deleteAccount: deleteAccountReducer(),
|
|
||||||
siteLanguage: siteLanguageReducer(),
|
siteLanguage: siteLanguageReducer(),
|
||||||
resetPassword: resetPasswordReducer(),
|
resetPassword: resetPasswordReducer(),
|
||||||
thirdPartyAuth: thirdPartyAuthReducer(),
|
thirdPartyAuth: thirdPartyAuthReducer(),
|
||||||
@@ -154,18 +152,6 @@ const reducer = (state = defaultState, action) => {
|
|||||||
// TODO: Once all the above cases have been converted into sub-reducers, we can use
|
// TODO: Once all the above cases have been converted into sub-reducers, we can use
|
||||||
// combineReducers in this file to greatly simplify it.
|
// combineReducers in this file to greatly simplify it.
|
||||||
|
|
||||||
// Delete My Account
|
|
||||||
case DELETE_ACCOUNT.CONFIRMATION:
|
|
||||||
case DELETE_ACCOUNT.BEGIN:
|
|
||||||
case DELETE_ACCOUNT.SUCCESS:
|
|
||||||
case DELETE_ACCOUNT.FAILURE:
|
|
||||||
case DELETE_ACCOUNT.RESET:
|
|
||||||
case DELETE_ACCOUNT.CANCEL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
deleteAccount: deleteAccountReducer(state.deleteAccount, action),
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_SITE_LANGUAGES.BEGIN:
|
case FETCH_SITE_LANGUAGES.BEGIN:
|
||||||
case FETCH_SITE_LANGUAGES.SUCCESS:
|
case FETCH_SITE_LANGUAGES.SUCCESS:
|
||||||
case FETCH_SITE_LANGUAGES.FAILURE:
|
case FETCH_SITE_LANGUAGES.FAILURE:
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import { usernameSelector, userRolesSelector, siteLanguageSelector } from './selectors';
|
import { usernameSelector, userRolesSelector, siteLanguageSelector } from './selectors';
|
||||||
|
|
||||||
// Sub-modules
|
// Sub-modules
|
||||||
import { saga as deleteAccountSaga } from './delete-account';
|
|
||||||
import { saga as resetPasswordSaga } from './reset-password';
|
import { saga as resetPasswordSaga } from './reset-password';
|
||||||
import { saga as siteLanguageSaga, ApiService as SiteLanguageApiService } from './site-language';
|
import { saga as siteLanguageSaga, ApiService as SiteLanguageApiService } from './site-language';
|
||||||
import { saga as thirdPartyAuthSaga } from './third-party-auth';
|
import { saga as thirdPartyAuthSaga } from './third-party-auth';
|
||||||
@@ -103,7 +102,6 @@ export default function* saga() {
|
|||||||
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
|
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
|
||||||
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
|
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
|
||||||
yield all([
|
yield all([
|
||||||
deleteAccountSaga(),
|
|
||||||
siteLanguageSaga(),
|
siteLanguageSaga(),
|
||||||
resetPasswordSaga(),
|
resetPasswordSaga(),
|
||||||
thirdPartyAuthSaga(),
|
thirdPartyAuthSaga(),
|
||||||
|
|||||||
42
src/common/hooks.js
Normal file
42
src/common/hooks.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const useAction = (action) => {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
|
const performAction = async (body = null) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setLoaded(false);
|
||||||
|
setData(null);
|
||||||
|
setError(null);
|
||||||
|
const result = await action(body);
|
||||||
|
setData(result);
|
||||||
|
setLoaded(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.response.data) {
|
||||||
|
setError(e.response.data);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
setLoaded(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAction = () => {
|
||||||
|
setLoading(false);
|
||||||
|
setLoaded(false);
|
||||||
|
setData(null);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [{
|
||||||
|
loaded, loading, data, error,
|
||||||
|
}, performAction, resetAction];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAction;
|
||||||
Reference in New Issue
Block a user