diff --git a/src/components/decryptMessage/decryptMessage.js b/src/components/decryptMessage/decryptMessage.js
new file mode 100644
index 000000000..8ca91ea13
--- /dev/null
+++ b/src/components/decryptMessage/decryptMessage.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import Input from 'react-toolbox/lib/input';
+import Lisk from 'lisk-js';
+import { translate } from 'react-i18next';
+import SignVerifyResult from '../signVerifyResult';
+import ActionBar from '../actionBar';
+
+class DecryptMessage extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ result: '',
+ nonce: {
+ value: '',
+ },
+ message: {
+ value: '',
+ },
+ senderPublicKey: {
+ value: '',
+ },
+ };
+ }
+
+ handleChange(name, value, error) {
+ this.setState({
+ [name]: {
+ value,
+ error,
+ },
+ });
+ }
+
+ showResult(event) {
+ event.preventDefault();
+ let decryptedMessage = null;
+ try {
+ decryptedMessage = Lisk.crypto.decryptMessageWithSecret(
+ this.state.message.value,
+ this.state.nonce.value,
+ this.props.account.passphrase,
+ this.state.senderPublicKey.value);
+ } catch (error) {
+ this.props.errorToast({ label: error.message });
+ }
+ if (decryptedMessage) {
+ const result = [
+ '-----DECRYPTED MESSAGE-----',
+ decryptedMessage,
+ ].join('\n');
+ this.setState({ result, resultIsShown: false });
+ this.setState({ resultIsShown: true });
+ this.props.successToast({ label: this.props.t('Message is decrypted successfully') });
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default translate()(DecryptMessage);
diff --git a/src/components/decryptMessage/decryptMessage.test.js b/src/components/decryptMessage/decryptMessage.test.js
new file mode 100644
index 000000000..121c1844e
--- /dev/null
+++ b/src/components/decryptMessage/decryptMessage.test.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import sinon from 'sinon';
+import { Provider } from 'react-redux';
+import { I18nextProvider } from 'react-i18next';
+import Lisk from 'lisk-js';
+import i18n from '../../i18n';
+import store from '../../store';
+import DecryptMessage from './decryptMessage';
+
+
+describe('DecryptMessage', () => {
+ let wrapper;
+ let successToastSpy;
+ let errorSpy;
+ let copyMock;
+ let decryptMessageMock;
+ const senderPublicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8';
+ const message = 'Hello world';
+ const decryptedMessage = 'Decrypted Hello world';
+ const nonce = 'this is nonce';
+ const publicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8';
+ const account = {
+ passphrase: 'wagon stock borrow episode laundry kitten salute link globe zero feed marble',
+ publicKey,
+ };
+
+ beforeEach(() => {
+ successToastSpy = sinon.spy();
+ errorSpy = sinon.spy();
+ copyMock = sinon.mock();
+ decryptMessageMock = sinon.stub(Lisk.crypto, 'decryptMessageWithSecret');
+ // decryptMessageSpy = sinon.spy(Lisk.crypto, 'decryptMessageWithSecret');
+ const props = {
+ account,
+ successToast: successToastSpy,
+ errorToast: sinon.spy(),
+ copyToClipboard: copyMock,
+ t: key => key,
+ };
+
+ wrapper = mount(
+
+
+
+ );
+ });
+
+ afterEach(() => {
+ decryptMessageMock.restore();
+ });
+
+ // ToDo find the problem with this test
+ it.skip('shows error toast when couldn\'t decrypt a message', () => {
+ decryptMessageMock.returnsPromise().rejects({ message: 'couldn\'t decrypt the message' });
+ wrapper.find('.message textarea').simulate('change', { target: { value: message } });
+ wrapper.find('.senderPublicKey input').simulate('change', { target: { value: senderPublicKey } });
+ wrapper.find('.nonce input').simulate('change', { target: { value: nonce } });
+ wrapper.find('form').simulate('submit');
+ expect(errorSpy).to.have.been.calledOnce();
+ expect(errorSpy).to.have.been.calledWith({ label: 'couldn\'t decrypt the message' });
+ });
+
+ it('allows to decrypt a message, copies encrypted message result to clipboard and shows success toast', () => {
+ copyMock.returns(true);
+ decryptMessageMock.returnsPromise().resolves(decryptedMessage);
+ wrapper.find('.message textarea').simulate('change', { target: { value: message } });
+ wrapper.find('.senderPublicKey input').simulate('change', { target: { value: senderPublicKey } });
+ wrapper.find('.nonce input').simulate('change', { target: { value: nonce } });
+ wrapper.find('form').simulate('submit');
+ expect(successToastSpy).to.have.been.calledWith({ label: 'Message is decrypted successfully' });
+ });
+
+ it('does not show success toast if copy-to-clipboard failed', () => {
+ copyMock.returns(false);
+ wrapper.find('.message textarea').simulate('change', { target: { value: message } });
+ wrapper.find('.senderPublicKey input').simulate('change', { target: { value: senderPublicKey } });
+ wrapper.find('.nonce input').simulate('change', { target: { value: nonce } });
+ wrapper.find('.primary-button').simulate('click');
+ expect(successToastSpy).to.have.not.been.calledWith();
+ });
+});
diff --git a/src/components/decryptMessage/index.js b/src/components/decryptMessage/index.js
new file mode 100644
index 000000000..5b95b0bf1
--- /dev/null
+++ b/src/components/decryptMessage/index.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+import copy from 'copy-to-clipboard';
+import { successToastDisplayed, errorToastDisplayed } from '../../actions/toaster';
+import DecryptMessage from './decryptMessage';
+
+const mapStateToProps = state => ({
+ account: state.account,
+});
+
+const mapDispatchToProps = dispatch => ({
+ successToast: data => dispatch(successToastDisplayed(data)),
+ errorToast: data => dispatch(errorToastDisplayed(data)),
+ copyToClipboard: (...args) => copy(...args),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(translate()(DecryptMessage));
diff --git a/src/components/decryptMessage/index.test.js b/src/components/decryptMessage/index.test.js
new file mode 100644
index 000000000..91cbd542d
--- /dev/null
+++ b/src/components/decryptMessage/index.test.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import sinon from 'sinon';
+import i18n from '../../i18n';
+import * as toasterActions from '../../actions/toaster';
+import store from '../../store';
+import DecryptMessageHOC from './index';
+import DecryptMessage from './decryptMessage';
+
+describe('DecryptMessageHOC', () => {
+ let props;
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount();
+ props = wrapper.find(DecryptMessage).props();
+ });
+
+ it('should render the decryptMessage with props.successToast and props.copyToClipboard and props.errorToast', () => {
+ expect(wrapper.find(DecryptMessage).exists()).to.equal(true);
+ expect(typeof wrapper.find(DecryptMessage).props().successToast).to.equal('function');
+ expect(typeof wrapper.find(DecryptMessage).props().copyToClipboard).to.equal('function');
+ });
+
+ it('should bind successToastDisplayed action to DecryptMessageComponent props.successToast', () => {
+ const actionsSpy = sinon.spy(toasterActions, 'successToastDisplayed');
+ props.successToast({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+
+ it('should bind errorToastDisplayed action to DecryptMessageComponent props.errorToast', () => {
+ const actionsSpy = sinon.spy(toasterActions, 'errorToastDisplayed');
+ props.errorToast({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+});
diff --git a/src/components/dialog/dialogs.js b/src/components/dialog/dialogs.js
index aeeb78683..95f1ea0fd 100644
--- a/src/components/dialog/dialogs.js
+++ b/src/components/dialog/dialogs.js
@@ -9,6 +9,8 @@ import Settings from '../settings';
import SignMessage from '../signMessage';
import VerifyMessage from '../verifyMessage';
import VoteDialog from '../voteDialog';
+import EncryptMessage from '../encryptMessage';
+import DecryptMessage from '../decryptMessage';
export default () => ({
send: {
@@ -51,4 +53,12 @@ export default () => ({
title: i18next.t('Settings'),
component: Settings,
},
+ 'encrypt-message': {
+ title: i18next.t('Encrypt message'),
+ component: EncryptMessage,
+ },
+ 'decrypt-message': {
+ title: i18next.t('Decrypt message'),
+ component: DecryptMessage,
+ },
});
diff --git a/src/components/encryptMessage/encryptMessage.js b/src/components/encryptMessage/encryptMessage.js
new file mode 100644
index 000000000..032c3a2f2
--- /dev/null
+++ b/src/components/encryptMessage/encryptMessage.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import Input from 'react-toolbox/lib/input';
+import Lisk from 'lisk-js';
+import { translate } from 'react-i18next';
+import InfoParagraph from '../infoParagraph';
+import SignVerifyResult from '../signVerifyResult';
+import ActionBar from '../actionBar';
+
+
+class EncryptMessage extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ result: '',
+ recipientPublicKey: {
+ value: '',
+ },
+ message: {
+ value: '',
+ },
+ };
+ }
+
+ handleChange(name, value, error) {
+ this.setState({
+ [name]: {
+ value,
+ error,
+ },
+ });
+ }
+
+ encrypt(event) {
+ event.preventDefault();
+ let cryptoResult = null;
+ try {
+ cryptoResult = Lisk.crypto.encryptMessageWithSecret(
+ this.state.message.value,
+ this.props.account.passphrase,
+ this.state.recipientPublicKey.value);
+ } catch (error) {
+ this.props.errorToast({ label: error.message });
+ }
+ if (cryptoResult) {
+ const result = [
+ '-----ENCRYPTED MESSAGE-----',
+ cryptoResult.encryptedMessage,
+ '-----NONCE-----',
+ cryptoResult.nonce,
+ ].join('\n');
+ this.setState({ result, resultIsShown: false });
+ this.showResult();
+ }
+ }
+
+ showResult() {
+ const copied = this.props.copyToClipboard(this.state.result, {
+ message: this.props.t('Press #{key} to copy'),
+ });
+ if (copied) {
+ this.props.successToast({ label: this.props.t('Result copied to clipboard') });
+ }
+ this.setState({ resultIsShown: true });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default translate()(EncryptMessage);
diff --git a/src/components/encryptMessage/encryptMessage.test.js b/src/components/encryptMessage/encryptMessage.test.js
new file mode 100644
index 000000000..f377984c7
--- /dev/null
+++ b/src/components/encryptMessage/encryptMessage.test.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import sinon from 'sinon';
+import { Provider } from 'react-redux';
+import { I18nextProvider } from 'react-i18next';
+import Lisk from 'lisk-js';
+import i18n from '../../i18n';
+import store from '../../store';
+import EncryptMessage from './encryptMessage';
+
+
+describe('EncryptMessage', () => {
+ let wrapper;
+ let successToastSpy;
+ let copyMock;
+ let encryptMessageSpy;
+ const recipientPublicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8';
+ const message = 'Hello world';
+ const publicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8';
+ const account = {
+ passphrase: 'wagon stock borrow episode laundry kitten salute link globe zero feed marble',
+ publicKey,
+ };
+
+ beforeEach(() => {
+ successToastSpy = sinon.spy();
+ copyMock = sinon.mock();
+ encryptMessageSpy = sinon.spy(Lisk.crypto, 'encryptMessageWithSecret');
+ const props = {
+ account,
+ successToast: successToastSpy,
+ copyToClipboard: copyMock,
+ t: key => key,
+ };
+
+ wrapper = mount(
+
+
+
+ );
+ });
+
+ afterEach(() => {
+ encryptMessageSpy.restore();
+ });
+
+ it('allows to encrypt a message, copies encrypted message result to clipboard and shows success toast', () => {
+ copyMock.returns(true);
+ wrapper.find('.message textarea').simulate('change', { target: { value: message } });
+ wrapper.find('.recipientPublicKey input').simulate('change', { target: { value: recipientPublicKey } });
+ wrapper.find('form').simulate('submit');
+ expect(encryptMessageSpy).to.have.been
+ .calledWith(message, account.passphrase, recipientPublicKey);
+ expect(successToastSpy).to.have.been.calledWith({ label: 'Result copied to clipboard' });
+ });
+
+ it('does not show success toast if copy-to-clipboard failed', () => {
+ copyMock.returns(false);
+ wrapper.find('.message textarea').simulate('change', { target: { value: message } });
+ wrapper.find('.recipientPublicKey input').simulate('change', { target: { value: recipientPublicKey } });
+ wrapper.find('.primary-button').simulate('click');
+ expect(successToastSpy).to.have.not.been.calledWith();
+ });
+});
diff --git a/src/components/encryptMessage/index.js b/src/components/encryptMessage/index.js
new file mode 100644
index 000000000..9522e2848
--- /dev/null
+++ b/src/components/encryptMessage/index.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+import copy from 'copy-to-clipboard';
+import { successToastDisplayed, errorToastDisplayed } from '../../actions/toaster';
+import EncryptMessage from './encryptMessage';
+
+const mapStateToProps = state => ({
+ account: state.account,
+});
+
+const mapDispatchToProps = dispatch => ({
+ successToast: data => dispatch(successToastDisplayed(data)),
+ errorToast: data => dispatch(errorToastDisplayed(data)),
+ copyToClipboard: (...args) => copy(...args),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(translate()(EncryptMessage));
diff --git a/src/components/encryptMessage/index.test.js b/src/components/encryptMessage/index.test.js
new file mode 100644
index 000000000..6c6867e98
--- /dev/null
+++ b/src/components/encryptMessage/index.test.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import sinon from 'sinon';
+import i18n from '../../i18n';
+import * as toasterActions from '../../actions/toaster';
+import store from '../../store';
+import EncryptMessageHOC from './index';
+import EncryptMessage from './encryptMessage';
+
+describe('EncryptMessageHOC', () => {
+ let props;
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount();
+ props = wrapper.find(EncryptMessage).props();
+ });
+
+ it('should render the encryptMessage with props.successToast and props.copyToClipboard and props.errorToast', () => {
+ expect(wrapper.find(EncryptMessage).exists()).to.equal(true);
+ expect(typeof wrapper.find(EncryptMessage).props().successToast).to.equal('function');
+ expect(typeof wrapper.find(EncryptMessage).props().copyToClipboard).to.equal('function');
+ });
+
+ it('should bind successToastDisplayed action to EncryptMessageComponent props.successToast', () => {
+ const actionsSpy = sinon.spy(toasterActions, 'successToastDisplayed');
+ props.successToast({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+
+ it('should bind errorToastDisplayed action to EncryptMessageComponent props.errorToast', () => {
+ const actionsSpy = sinon.spy(toasterActions, 'errorToastDisplayed');
+ props.errorToast({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+});
diff --git a/src/components/header/header.js b/src/components/header/header.js
index d0c471acd..c1a161ca1 100644
--- a/src/components/header/header.js
+++ b/src/components/header/header.js
@@ -44,6 +44,14 @@ const Header = props => (
{props.t('Verify message')}
+
+