mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Changing cancel button labels in send screen when in editing stage (#12056)
* Changing cancel button labels in send screen when in editing stage * Re-route user to home page when reject is clicked on edit transaction page
This commit is contained in:
parent
ae84b0cb6d
commit
ade9e4ac4b
@ -305,6 +305,9 @@
|
|||||||
"cancel": {
|
"cancel": {
|
||||||
"message": "Cancel"
|
"message": "Cancel"
|
||||||
},
|
},
|
||||||
|
"cancelEdit": {
|
||||||
|
"message": "Cancel Edit"
|
||||||
|
},
|
||||||
"cancelPopoverTitle": {
|
"cancelPopoverTitle": {
|
||||||
"message": "Cancel transaction"
|
"message": "Cancel transaction"
|
||||||
},
|
},
|
||||||
|
@ -1736,6 +1736,10 @@ export function getSendHexData(state) {
|
|||||||
return state[name].draftTransaction.userInputHexData;
|
return state[name].draftTransaction.userInputHexData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDraftTransactionID(state) {
|
||||||
|
return state[name].draftTransaction.id;
|
||||||
|
}
|
||||||
|
|
||||||
export function sendAmountIsInError(state) {
|
export function sendAmountIsInError(state) {
|
||||||
return Boolean(state[name].amount.error);
|
return Boolean(state[name].amount.error);
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ import sendReducer, {
|
|||||||
getSendAmount,
|
getSendAmount,
|
||||||
getIsBalanceInsufficient,
|
getIsBalanceInsufficient,
|
||||||
getSendMaxModeState,
|
getSendMaxModeState,
|
||||||
|
getDraftTransactionID,
|
||||||
sendAmountIsInError,
|
sendAmountIsInError,
|
||||||
getSendHexData,
|
getSendHexData,
|
||||||
getSendTo,
|
getSendTo,
|
||||||
@ -2459,6 +2460,21 @@ describe('Send Slice', () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has a selector to get the draft transaction ID', () => {
|
||||||
|
expect(getDraftTransactionID({ send: initialState })).toBeNull();
|
||||||
|
expect(
|
||||||
|
getDraftTransactionID({
|
||||||
|
send: {
|
||||||
|
...initialState,
|
||||||
|
draftTransaction: {
|
||||||
|
...initialState.draftTransaction,
|
||||||
|
id: 'ID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe('ID');
|
||||||
|
});
|
||||||
|
|
||||||
it('has a selector to get the user entered hex data', () => {
|
it('has a selector to get the user entered hex data', () => {
|
||||||
expect(getSendHexData({ send: initialState })).toBeNull();
|
expect(getSendHexData({ send: initialState })).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
|
@ -2,7 +2,11 @@ import React, { Component } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import PageContainerFooter from '../../../components/ui/page-container/page-container-footer';
|
import PageContainerFooter from '../../../components/ui/page-container/page-container-footer';
|
||||||
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes';
|
import {
|
||||||
|
CONFIRM_TRANSACTION_ROUTE,
|
||||||
|
DEFAULT_ROUTE,
|
||||||
|
} from '../../../helpers/constants/routes';
|
||||||
|
import { SEND_STAGES } from '../../../ducks/send';
|
||||||
|
|
||||||
export default class SendFooter extends Component {
|
export default class SendFooter extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -13,9 +17,12 @@ export default class SendFooter extends Component {
|
|||||||
sign: PropTypes.func,
|
sign: PropTypes.func,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
toAccounts: PropTypes.array,
|
toAccounts: PropTypes.array,
|
||||||
|
sendStage: PropTypes.string,
|
||||||
sendErrors: PropTypes.object,
|
sendErrors: PropTypes.object,
|
||||||
gasEstimateType: PropTypes.string,
|
gasEstimateType: PropTypes.string,
|
||||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||||
|
cancelTx: PropTypes.func,
|
||||||
|
draftTransactionID: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -24,9 +31,21 @@ export default class SendFooter extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onCancel() {
|
onCancel() {
|
||||||
const { resetSendState, history, mostRecentOverviewPage } = this.props;
|
const {
|
||||||
|
cancelTx,
|
||||||
|
draftTransactionID,
|
||||||
|
history,
|
||||||
|
mostRecentOverviewPage,
|
||||||
|
resetSendState,
|
||||||
|
sendStage,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (draftTransactionID) cancelTx({ id: draftTransactionID });
|
||||||
resetSendState();
|
resetSendState();
|
||||||
history.push(mostRecentOverviewPage);
|
|
||||||
|
const nextRoute =
|
||||||
|
sendStage === SEND_STAGES.EDIT ? DEFAULT_ROUTE : mostRecentOverviewPage;
|
||||||
|
history.push(nextRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onSubmit(event) {
|
async onSubmit(event) {
|
||||||
@ -85,11 +104,14 @@ export default class SendFooter extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { t } = this.context;
|
||||||
|
const { sendStage } = this.props;
|
||||||
return (
|
return (
|
||||||
<PageContainerFooter
|
<PageContainerFooter
|
||||||
onCancel={() => this.onCancel()}
|
onCancel={() => this.onCancel()}
|
||||||
onSubmit={(e) => this.onSubmit(e)}
|
onSubmit={(e) => this.onSubmit(e)}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
|
cancelText={sendStage === SEND_STAGES.EDIT ? t('reject') : t('cancel')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes';
|
import {
|
||||||
|
CONFIRM_TRANSACTION_ROUTE,
|
||||||
|
DEFAULT_ROUTE,
|
||||||
|
} from '../../../helpers/constants/routes';
|
||||||
import PageContainerFooter from '../../../components/ui/page-container/page-container-footer';
|
import PageContainerFooter from '../../../components/ui/page-container/page-container-footer';
|
||||||
|
import { renderWithProvider } from '../../../../test/jest';
|
||||||
import SendFooter from './send-footer.component';
|
import SendFooter from './send-footer.component';
|
||||||
|
|
||||||
describe('SendFooter Component', () => {
|
describe('SendFooter Component', () => {
|
||||||
@ -10,6 +14,7 @@ describe('SendFooter Component', () => {
|
|||||||
|
|
||||||
const propsMethodSpies = {
|
const propsMethodSpies = {
|
||||||
addToAddressBookIfNew: sinon.spy(),
|
addToAddressBookIfNew: sinon.spy(),
|
||||||
|
cancelTx: sinon.spy(),
|
||||||
resetSendState: sinon.spy(),
|
resetSendState: sinon.spy(),
|
||||||
sign: sinon.spy(),
|
sign: sinon.spy(),
|
||||||
update: sinon.spy(),
|
update: sinon.spy(),
|
||||||
@ -20,31 +25,40 @@ describe('SendFooter Component', () => {
|
|||||||
};
|
};
|
||||||
const MOCK_EVENT = { preventDefault: () => undefined };
|
const MOCK_EVENT = { preventDefault: () => undefined };
|
||||||
|
|
||||||
|
const renderShallow = (props) => {
|
||||||
|
return shallow(
|
||||||
|
<SendFooter
|
||||||
|
addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
|
||||||
|
resetSendState={propsMethodSpies.resetSendState}
|
||||||
|
cancelTx={propsMethodSpies.cancelTx}
|
||||||
|
disabled
|
||||||
|
draftTransactionID="ID"
|
||||||
|
history={historySpies}
|
||||||
|
sign={propsMethodSpies.sign}
|
||||||
|
to="mockTo"
|
||||||
|
toAccounts={['mockAccount']}
|
||||||
|
sendErrors={{}}
|
||||||
|
sendStage="DRAFT"
|
||||||
|
gasEstimateType="BASIC"
|
||||||
|
mostRecentOverviewPage="mostRecentOverviewPage"
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
{ context: { t: (str) => str, metricsEvent: () => ({}) } },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
sinon.spy(SendFooter.prototype, 'onCancel');
|
sinon.spy(SendFooter.prototype, 'onCancel');
|
||||||
sinon.spy(SendFooter.prototype, 'onSubmit');
|
sinon.spy(SendFooter.prototype, 'onSubmit');
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(
|
wrapper = renderShallow();
|
||||||
<SendFooter
|
|
||||||
addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
|
|
||||||
resetSendState={propsMethodSpies.resetSendState}
|
|
||||||
disabled
|
|
||||||
history={historySpies}
|
|
||||||
sign={propsMethodSpies.sign}
|
|
||||||
to="mockTo"
|
|
||||||
toAccounts={['mockAccount']}
|
|
||||||
sendErrors={{}}
|
|
||||||
gasEstimateType="BASIC"
|
|
||||||
mostRecentOverviewPage="mostRecentOverviewPage"
|
|
||||||
/>,
|
|
||||||
{ context: { t: (str) => str, metricsEvent: () => ({}) } },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
propsMethodSpies.resetSendState.resetHistory();
|
propsMethodSpies.resetSendState.resetHistory();
|
||||||
|
propsMethodSpies.cancelTx.resetHistory();
|
||||||
propsMethodSpies.addToAddressBookIfNew.resetHistory();
|
propsMethodSpies.addToAddressBookIfNew.resetHistory();
|
||||||
propsMethodSpies.resetSendState.resetHistory();
|
propsMethodSpies.resetSendState.resetHistory();
|
||||||
propsMethodSpies.sign.resetHistory();
|
propsMethodSpies.sign.resetHistory();
|
||||||
@ -65,6 +79,15 @@ describe('SendFooter Component', () => {
|
|||||||
expect(propsMethodSpies.resetSendState.callCount).toStrictEqual(1);
|
expect(propsMethodSpies.resetSendState.callCount).toStrictEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call cancelTx', () => {
|
||||||
|
expect(propsMethodSpies.cancelTx.callCount).toStrictEqual(0);
|
||||||
|
wrapper.instance().onCancel();
|
||||||
|
expect(propsMethodSpies.cancelTx.callCount).toStrictEqual(1);
|
||||||
|
expect(propsMethodSpies.cancelTx.getCall(0).args[0]?.id).toStrictEqual(
|
||||||
|
'ID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should call history.push', () => {
|
it('should call history.push', () => {
|
||||||
expect(historySpies.push.callCount).toStrictEqual(0);
|
expect(historySpies.push.callCount).toStrictEqual(0);
|
||||||
wrapper.instance().onCancel();
|
wrapper.instance().onCancel();
|
||||||
@ -73,6 +96,14 @@ describe('SendFooter Component', () => {
|
|||||||
'mostRecentOverviewPage',
|
'mostRecentOverviewPage',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call history.push with DEFAULT_ROUTE in edit stage', () => {
|
||||||
|
wrapper = renderShallow({ sendStage: 'EDIT' });
|
||||||
|
expect(historySpies.push.callCount).toStrictEqual(0);
|
||||||
|
wrapper.instance().onCancel();
|
||||||
|
expect(historySpies.push.callCount).toStrictEqual(1);
|
||||||
|
expect(historySpies.push.getCall(0).args[0]).toStrictEqual(DEFAULT_ROUTE);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onSubmit', () => {
|
describe('onSubmit', () => {
|
||||||
@ -107,7 +138,9 @@ describe('SendFooter Component', () => {
|
|||||||
addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
|
addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
|
||||||
amount="mockAmount"
|
amount="mockAmount"
|
||||||
resetSendState={propsMethodSpies.resetSendState}
|
resetSendState={propsMethodSpies.resetSendState}
|
||||||
|
cancelTx={propsMethodSpies.cancelTx}
|
||||||
disabled
|
disabled
|
||||||
|
draftTransactionID="ID"
|
||||||
editingTransactionId="mockEditingTransactionId"
|
editingTransactionId="mockEditingTransactionId"
|
||||||
errors={{}}
|
errors={{}}
|
||||||
from={{ address: 'mockAddress', balance: 'mockBalance' }}
|
from={{ address: 'mockAddress', balance: 'mockBalance' }}
|
||||||
@ -147,4 +180,28 @@ describe('SendFooter Component', () => {
|
|||||||
expect(SendFooter.prototype.onCancel.callCount).toStrictEqual(1);
|
expect(SendFooter.prototype.onCancel.callCount).toStrictEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Cancel Button', () => {
|
||||||
|
const renderFooter = (props) =>
|
||||||
|
renderWithProvider(
|
||||||
|
<SendFooter
|
||||||
|
disabled
|
||||||
|
mostRecentOverviewPage="mostRecentOverviewPage"
|
||||||
|
draftTransactionID="ID"
|
||||||
|
sendErrors={{}}
|
||||||
|
sendStage="DRAFT"
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
it('has a cancel button in footer', () => {
|
||||||
|
const { getByText } = renderFooter();
|
||||||
|
expect(getByText('Cancel')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has label changed to Reject in editing stage', () => {
|
||||||
|
const { getByText } = renderFooter({ sendStage: 'EDIT' });
|
||||||
|
expect(getByText('Reject')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { addToAddressBook } from '../../../store/actions';
|
import { addToAddressBook, cancelTx } from '../../../store/actions';
|
||||||
import {
|
import {
|
||||||
getRenderableEstimateDataForSmallButtonsFromGWEI,
|
getRenderableEstimateDataForSmallButtonsFromGWEI,
|
||||||
getDefaultActiveButtonIndex,
|
getDefaultActiveButtonIndex,
|
||||||
@ -7,10 +7,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
resetSendState,
|
resetSendState,
|
||||||
getGasPrice,
|
getGasPrice,
|
||||||
|
getSendStage,
|
||||||
getSendTo,
|
getSendTo,
|
||||||
getSendErrors,
|
getSendErrors,
|
||||||
isSendFormInvalid,
|
isSendFormInvalid,
|
||||||
signTransaction,
|
signTransaction,
|
||||||
|
getDraftTransactionID,
|
||||||
} from '../../../ducks/send';
|
} from '../../../ducks/send';
|
||||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||||
import { addHexPrefix } from '../../../../app/scripts/lib/util';
|
import { addHexPrefix } from '../../../../app/scripts/lib/util';
|
||||||
@ -43,7 +45,9 @@ function mapStateToProps(state) {
|
|||||||
disabled: isSendFormInvalid(state),
|
disabled: isSendFormInvalid(state),
|
||||||
to: getSendTo(state),
|
to: getSendTo(state),
|
||||||
toAccounts: getSendToAccounts(state),
|
toAccounts: getSendToAccounts(state),
|
||||||
|
sendStage: getSendStage(state),
|
||||||
sendErrors: getSendErrors(state),
|
sendErrors: getSendErrors(state),
|
||||||
|
draftTransactionID: getDraftTransactionID(state),
|
||||||
gasEstimateType,
|
gasEstimateType,
|
||||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||||
};
|
};
|
||||||
@ -52,6 +56,7 @@ function mapStateToProps(state) {
|
|||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
resetSendState: () => dispatch(resetSendState()),
|
resetSendState: () => dispatch(resetSendState()),
|
||||||
|
cancelTx: (t) => dispatch(cancelTx(t)),
|
||||||
sign: () => dispatch(signTransaction()),
|
sign: () => dispatch(signTransaction()),
|
||||||
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
|
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
|
||||||
const hexPrefixedAddress = addHexPrefix(newAddress);
|
const hexPrefixedAddress = addHexPrefix(newAddress);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import { addToAddressBook } from '../../../store/actions';
|
import { addToAddressBook, cancelTx } from '../../../store/actions';
|
||||||
import { resetSendState, signTransaction } from '../../../ducks/send';
|
import { resetSendState, signTransaction } from '../../../ducks/send';
|
||||||
|
|
||||||
let mapDispatchToProps;
|
let mapDispatchToProps;
|
||||||
@ -14,6 +14,7 @@ jest.mock('react-redux', () => ({
|
|||||||
|
|
||||||
jest.mock('../../../store/actions.js', () => ({
|
jest.mock('../../../store/actions.js', () => ({
|
||||||
addToAddressBook: jest.fn(),
|
addToAddressBook: jest.fn(),
|
||||||
|
cancelTx: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../../ducks/metamask/metamask', () => ({
|
jest.mock('../../../ducks/metamask/metamask', () => ({
|
||||||
@ -24,6 +25,8 @@ jest.mock('../../../ducks/send', () => ({
|
|||||||
getGasPrice: (s) => `mockGasPrice:${s}`,
|
getGasPrice: (s) => `mockGasPrice:${s}`,
|
||||||
getSendTo: (s) => `mockTo:${s}`,
|
getSendTo: (s) => `mockTo:${s}`,
|
||||||
getSendErrors: (s) => `mockSendErrors:${s}`,
|
getSendErrors: (s) => `mockSendErrors:${s}`,
|
||||||
|
getSendStage: (s) => `mockStage:${s}`,
|
||||||
|
getDraftTransaction: (s) => ({ id: `draftTransaction:${s}` }),
|
||||||
resetSendState: jest.fn(),
|
resetSendState: jest.fn(),
|
||||||
signTransaction: jest.fn(),
|
signTransaction: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@ -53,6 +56,16 @@ describe('send-footer container', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cancelTx()', () => {
|
||||||
|
it('should dispatch an action', () => {
|
||||||
|
const draftTansaction = { id: 'ID' };
|
||||||
|
mapDispatchToPropsObject.cancelTx(draftTansaction);
|
||||||
|
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||||
|
expect(cancelTx).toHaveBeenCalledTimes(1);
|
||||||
|
expect(cancelTx).toHaveBeenCalledWith(draftTansaction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('sign()', () => {
|
describe('sign()', () => {
|
||||||
it('should dispatch a signTransaction action', () => {
|
it('should dispatch a signTransaction action', () => {
|
||||||
mapDispatchToPropsObject.sign();
|
mapDispatchToPropsObject.sign();
|
||||||
|
@ -38,7 +38,9 @@ export default function SendHeader() {
|
|||||||
className="send__header"
|
className="send__header"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={title}
|
title={title}
|
||||||
headerCloseText={t('cancel')}
|
headerCloseText={
|
||||||
|
stage === SEND_STAGES.EDIT ? t('cancelEdit') : t('cancel')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,18 @@ describe('SendHeader Component', () => {
|
|||||||
expect(getByText('Cancel')).toBeTruthy();
|
expect(getByText('Cancel')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has button label changed to Cancel Edit in editing stage', () => {
|
||||||
|
const { getByText } = renderWithProvider(
|
||||||
|
<SendHeader />,
|
||||||
|
configureMockStore(middleware)({
|
||||||
|
send: { ...initialState, stage: SEND_STAGES.EDIT },
|
||||||
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(getByText('Cancel Edit')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('resets send state when clicked', () => {
|
it('resets send state when clicked', () => {
|
||||||
const store = configureMockStore(middleware)({
|
const store = configureMockStore(middleware)({
|
||||||
send: initialState,
|
send: initialState,
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
right: 1rem;
|
right: 1rem;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user