diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json
index 8580be8774..73415ae19c 100644
--- a/i18n/locales/en/common.json
+++ b/i18n/locales/en/common.json
@@ -15,8 +15,7 @@
"Add a Lisk ID": "Add a Lisk ID",
"Add new": "Add new",
"Add this account to your dashboard to keep track of its balance, and use it as a bookmark in the future.": "Add this account to your dashboard to keep track of its balance, and use it as a bookmark in the future.",
- "Add to dashboard": "Add to dashboard",
- "Add to followed accounts": "Add to followed accounts",
+ "Add to bookmarks": "Add to bookmarks",
"Add to list": "Add to list",
"Added votes": "Added votes",
"Additional fee": "Additional fee",
@@ -126,7 +125,6 @@
"Filter votes": "Filter votes",
"Final confirmation": "Final confirmation",
"Follow": "Follow",
- "Follow Account": "Follow Account",
"Following": "Following",
"Get passphrase": "Get passphrase",
"Get to your Dashboard": "Get to your Dashboard",
diff --git a/jest.config.js b/jest.config.js
index e454e878cb..599b9f751c 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -82,6 +82,11 @@ module.exports = {
'src/utils/proxyLogin.js',
'src/utils/rawTransactionWrapper.js',
'src/utils/to.js',
+ 'src/components/resultBox/resultBox.js', // FollowUp #1515
+ 'src/components/passphraseSteps/index.js', // FollowUp #1515
+ 'src/actions/peers.js', // FollowUp #1515
+ 'src/components/send/steps/confirm/confirm.js', // FollowUp #1515
+ 'src/components/sendNew/',
],
coverageThreshold: {
global: {
diff --git a/src/components/followedAccounts/accountTitleInput.js b/src/components/followedAccounts/accountTitleInput.js
index 717494fc29..9f5e1abda0 100644
--- a/src/components/followedAccounts/accountTitleInput.js
+++ b/src/components/followedAccounts/accountTitleInput.js
@@ -16,7 +16,6 @@ const AccountTitleInput = ({
autoFocus={true}
disabled={disabled}
onChange={val => onChange(val, validateInput)}
- require={true}
/>;
};
diff --git a/src/components/multiStep/index.js b/src/components/multiStep/index.js
index 68453975fd..acb1df9c9d 100644
--- a/src/components/multiStep/index.js
+++ b/src/components/multiStep/index.js
@@ -82,15 +82,11 @@ class MultiStep extends React.Component {
prevStep: step.prevStep,
reset: this.reset.bind(this),
...step.data[step.current],
+ finalCallback,
};
- if (step.current === (children.length - 1)) {
- if (typeof finalCallback === 'function') {
- extraProps.finalCallback = finalCallback;
- }
- } else {
- extraProps.prevState = Object.assign({}, step.data[step.current + 1]);
- }
+ extraProps.prevState = Object.assign({}, step.data[step.current + 1]);
+
return (
h2 {
+ font-size: 28px;
+ }
+
& footer {
padding: 0px;
+ display: flex;
+ justify-content: center;
+
+ & > button {
+ width: calc(100% / 2 - 20px);
+ margin: 0;
+ }
}
& p {
color: var(--paragraph-color);
line-height: var(--paragraph-line-height);
+ font-size: 16px;
}
& .copy {
color: var(--copy-color);
cursor: pointer;
- }
-}
-
-.okButton {
- margin-bottom: 20px;
-}
-
-@media (--small-viewport) {
- .resultBox {
- height: calc(100vh - 58px - 62px); /* stylelint-disable-line */
- }
-
- .icon {
- margin-top: 40px;
- }
-}
-
-@media (--xSmall-viewport) {
- .okButton {
- width: 100%;
+ font-size: 16px;
+ font-weight: 600;
}
- .addFollowedAccountButton {
- width: 100%;
+ & .okButton {
+ margin-left: 20px;
}
}
diff --git a/src/components/resultBox/resultBox.js b/src/components/resultBox/resultBox.js
index ac8efb16e3..3f70eb0f7c 100644
--- a/src/components/resultBox/resultBox.js
+++ b/src/components/resultBox/resultBox.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { Button } from '../toolbox/buttons/button';
+import { Button, ActionButton } from '../toolbox/buttons/button';
import { FontIcon } from '../fontIcon';
import CopyToClipboard from '../copyToClipboard';
@@ -45,29 +45,17 @@ class ResultBox extends React.Component {
diff --git a/src/components/resultBox/resultBox.test.js b/src/components/resultBox/resultBox.test.js
index 016d2b335f..1ba67b87a8 100644
--- a/src/components/resultBox/resultBox.test.js
+++ b/src/components/resultBox/resultBox.test.js
@@ -117,6 +117,6 @@ describe('Result Box', () => {
wrapper = mount(, options);
- expect(wrapper).to.have.descendants('.add-follwed-account-button');
+ expect(wrapper).to.have.descendants('.add-to-bookmarks');
});
});
diff --git a/src/components/send/index.js b/src/components/send/index.js
index 7035e18ecd..e6cf45ccd4 100644
--- a/src/components/send/index.js
+++ b/src/components/send/index.js
@@ -105,4 +105,3 @@ class Send extends React.Component {
}
export default translate()(Send);
-
diff --git a/src/components/sendNew/index.js b/src/components/sendNew/index.js
new file mode 100644
index 0000000000..0c3cd4e470
--- /dev/null
+++ b/src/components/sendNew/index.js
@@ -0,0 +1,89 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+import grid from 'flexboxgrid/dist/flexboxgrid.css';
+
+// import { FontIcon } from '../fontIcon';
+import Box from '../box';
+import MultiStep from './../multiStep';
+import ResultBox from '../resultBox';
+import Form from './steps/form';
+import Confirm from './steps/confirm';
+import FollowAccount from '../sendTo/followAccount';
+import PassphraseSteps from './../passphraseSteps';
+import AccountInitialization from '../accountInitialization';
+import { parseSearchParams } from './../../utils/searchParams';
+import breakpoints from './../../constants/breakpoints';
+import styles from './send.css';
+
+class Send extends React.Component {
+ constructor(props) {
+ super(props);
+ const needsAccountInit = !props.account.serverPublicKey
+ && props.account.balance > 0
+ && props.pendingTransactions.length === 0;
+
+ const { amount, recipient } = this.getSearchParams();
+ this.state = {
+ isActiveOnMobile: !!recipient || !!amount || needsAccountInit,
+ isActiveTabSend: true,
+ };
+ }
+
+ getSearchParams() {
+ return parseSearchParams(this.props.history.location.search);
+ }
+
+ setActiveOnMobile({ isActiveOnMobile, isActiveTabSend = true }) {
+ this.setState({ isActiveOnMobile, isActiveTabSend });
+ }
+
+ setActiveTabSend(isActiveTabSend) {
+ this.setState({ isActiveTabSend });
+ }
+
+ goToWallet() {
+ this.props.history.push('/wallet');
+ }
+
+ render() {
+ const { amount, recipient, reference } = this.getSearchParams();
+
+ return (
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ account: state.account,
+});
+
+export default connect(mapStateToProps)(translate()(Send));
+
diff --git a/src/components/sendNew/send.css b/src/components/sendNew/send.css
new file mode 100644
index 0000000000..93616264aa
--- /dev/null
+++ b/src/components/sendNew/send.css
@@ -0,0 +1,82 @@
+@import '../app/variables';
+
+.send {
+ position: relative;
+ background: linear-gradient(90deg, #fff, #f5f8fc);
+}
+
+.mobileMenu,
+.mobileClose {
+ display: none;
+}
+
+@media (--medium-viewport) {
+ .send {
+ position: fixed;
+ right: 0;
+ height: 100vh; /* stylelint-disable-line */
+ transition: top 300ms ease-in-out;
+ z-index: 10;
+
+ &.isActive {
+ top: var(--m-top-bar-height);
+
+ & .wrapper {
+ padding-bottom: 100px;
+ }
+ }
+ }
+
+ .mobileClose {
+ display: block;
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ }
+
+ .mobileMenu {
+ display: block;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: 5;
+ }
+
+ .mobileMenuItem {
+ display: inline-block;
+ padding: 20px;
+ font-weight: 600;
+ font-family: var(--heading-font);
+ cursor: pointer;
+ font-size: 24px;
+ }
+}
+
+@media (--small-viewport) {
+ .send {
+ height: 100vh; /* stylelint-disable-line */
+
+ &.isActive {
+ top: var(--s-top-bar-height);
+
+ & .wrapper {
+ padding-bottom: 50px;
+ }
+ }
+ }
+
+ .mobileMenuItem {
+ font-size: 18px;
+ }
+}
+
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ justify-content: center;
+}
diff --git a/src/components/sendNew/steps/confirm/confirm.css b/src/components/sendNew/steps/confirm/confirm.css
new file mode 100644
index 0000000000..1f3a8d5099
--- /dev/null
+++ b/src/components/sendNew/steps/confirm/confirm.css
@@ -0,0 +1,221 @@
+@import '../../../app/variables.css';
+
+:root {
+ --tab-inactive-color: --color-grayscale-dark;
+ --font-size-L: 16px;
+ --font-size-XS: 14px;
+}
+
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ width: 100%;
+ box-sizing: border-box;
+ text-align: left;
+ margin: 113px 0 0;
+ padding: 0;
+
+ & p {
+ font-size: var(--font-size-L);
+ line-height: 30px;
+ }
+
+ & > form {
+ margin-top: 54px;
+
+ & > div {
+ height: 82px;
+
+ & input {
+ margin-top: 1px;
+ font-size: 16px;
+ font-weight: 600;
+ }
+ }
+
+ & > div:first-child {
+ position: relative;
+
+ & > label {
+ top: 0;
+ }
+
+ & > div {
+ position: absolute;
+ top: 24px;
+ }
+ }
+
+ & > div:nth-child(2) {
+ margin-top: 25px;
+
+ & textarea {
+ font-weight: 600;
+ color: var(--color-grayscale-dark);
+ }
+ }
+
+ & label {
+ font-weight: 600;
+ }
+
+ & input {
+ font-weight: 600;
+ }
+ }
+
+ & > footer {
+ width: 100%;
+ margin: 23px 0 0;
+
+ & > div {
+ height: 80px;
+
+ & input {
+ margin-top: 1px;
+ }
+ }
+
+ & > section {
+ width: 100%;
+ }
+
+ & > section > button {
+ width: 160px;
+ height: 56px;
+ }
+
+ & > section > button:last-child {
+ margin-left: 22px;
+ height: 57px;
+ }
+ }
+}
+
+.recepientRow {
+ display: flex;
+}
+
+.disabled > .inputElement {
+ border-bottom: none;
+ color: #212121;
+ overflow: auto;
+ resize: none;
+}
+
+.fee {
+ text-align: right;
+ margin-top: -12px;
+ margin-bottom: 24px;
+ font-size: 12px;
+ line-height: 14px;
+ color: grey;
+}
+
+input:read-only {
+ border-bottom: solid 1px var(--color-white) !important;
+}
+
+.disabledInput label {
+ font-size: 14px;
+}
+
+.button {
+ min-width: 0;
+ width: 100%;
+}
+
+.headerWrapper {
+ padding: 0 !important;
+ margin: 0;
+
+ & .title {
+ float: left;
+ }
+
+ & .account {
+ float: right;
+ }
+
+ & h2 {
+ margin: 0;
+ }
+
+ & h3 {
+ display: inline-block;
+ margin: 0;
+ }
+
+ & .balanceUnit {
+ font-weight: var(--font-weight-bold);
+ }
+
+ & .address {
+ text-align: right;
+ cursor: pointer;
+ }
+
+ & .transfer {
+ color: var(--color-grayscale-dark);
+ }
+}
+
+.accountVisual {
+ margin-right: 10px;
+}
+
+.text {
+ color: #3c5068;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.smallAddress {
+ margin-top: 9px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.address {
+ font-size: 16px;
+ font-weight: 600;
+ padding-top: 10px;
+}
+
+.sendTo {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+}
+
+.disabledInput input:disabled {
+ opacity: 1;
+ border: none;
+}
+
+.subTitle {
+ font-size: 14px;
+}
+
+@media (--small-viewport) {
+ .headerWrapper {
+ display: none;
+ }
+
+ .header {
+ height: 60px;
+ }
+
+ .accountVisual {
+ margin: 24px 0;
+ }
+}
+
+@media (--xSmall-viewport) {
+ .wrapper {
+ & p {
+ font-size: var(--font-size-L);
+ }
+ }
+}
diff --git a/src/components/sendNew/steps/confirm/confirm.js b/src/components/sendNew/steps/confirm/confirm.js
new file mode 100644
index 0000000000..668cd8eb42
--- /dev/null
+++ b/src/components/sendNew/steps/confirm/confirm.js
@@ -0,0 +1,197 @@
+import React from 'react';
+import { fromRawLsk, toRawLsk } from '../../../../utils/lsk';
+import AccountVisual from '../../../accountVisual';
+import { Button, PrimaryButton } from './../../../toolbox/buttons/button';
+import ToolBoxInput from '../../../toolbox/inputs/toolBoxInput';
+import fees from './../../../../constants/fees';
+import styles from './confirm.css';
+
+class Confirm extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ recipient: {
+ value: '',
+ },
+ amount: {
+ value: '',
+ },
+ reference: {
+ value: '',
+ },
+ loading: false,
+ };
+ this.fee = fees.send;
+ }
+
+ componentDidMount() {
+ const recipient = this.props.accountInit ? this.props.account.address : this.props.recipient;
+ const amount = this.props.accountInit ? 0.1 : this.props.amount;
+ const newState = {
+ recipient: {
+ value: recipient || '',
+ },
+ amount: {
+ value: amount || '',
+ },
+ reference: {
+ value: this.props.reference || '',
+ },
+ };
+ this.setState(newState);
+ }
+
+ componentDidUpdate() {
+ // Hardware wallet code preventing by going on last step when there is pending transaction
+ const pending = this.props.account.hwInfo && this.props.account.hwInfo.deviceId
+ ? this.props.pendingTransactions.find(transaction => (
+ transaction.senderId === this.props.account.address &&
+ transaction.recipientId === this.state.recipient.value &&
+ fromRawLsk(transaction.amount) === this.props.amount
+ )) : this.props.pendingTransactions.length;
+
+ if (this.state.loading && (pending || this.props.failedTransactions)) {
+ const data = this.getTransactionState();
+ this.props.nextStep({
+ ...data,
+ amount: this.props.amount,
+ account: this.props.account,
+ recipientId: this.state.recipient.value,
+ });
+ this.setState({ loading: false });
+ }
+ }
+
+ getTransactionState() {
+ const success = this.props.pendingTransactions.length > 0 && !this.props.failedTransactions;
+ const successMessage = this.props.t('Transaction is being processed and will be confirmed. It may take up to 15 minutes to be secured in the blockchain.');
+ const failureMessage = this.props.failedTransactions ? this.props.failedTransactions.errorMessage : '';
+ const copy = success ? {
+ title: this.props.t('Copy Transaction ID to clipboard'),
+ value: this.props.pendingTransactions[0].id,
+ } : null;
+ return {
+ title: success ? this.props.t('Thank you') : this.props.t('Sorry'),
+ body: success ? successMessage : failureMessage,
+ copy,
+ success,
+ };
+ }
+
+ handleChange(name, value, error) {
+ this.setState({
+ [name]: {
+ value,
+ error: typeof error === 'string' ? error : '',
+ },
+ });
+ }
+
+ send(event) {
+ event.preventDefault();
+ this.setState({ loading: true });
+ this.props.sent({
+ account: this.props.account,
+ recipientId: this.state.recipient.value,
+ amount: this.state.amount.value,
+ passphrase: this.props.passphrase.value,
+ secondPassphrase: this.props.secondPassphrase.value,
+ data: this.props.accountInit ? this.props.t('Account initialization') : this.props.reference,
+ });
+ }
+
+ addAmountAndFee() {
+ return fromRawLsk(toRawLsk(this.state.amount.value) + this.fee);
+ }
+
+ render() {
+ const followedAccount = this.props.followedAccounts
+ .find(account => account.address === this.state.recipient.value);
+ return (
+
+
+ {this.props.t('Send LSK')}
+
+ {this.props.accountInit
+ ?
+
{this.props.t('You will send a small amount of {{fee}} LSK to yourself and therefore initialize your ID.', { fee: fromRawLsk(fees.send) })}
+
{this.props.t('You only need to do this once for each Lisk ID.')}
+
+ :
+ }
+
+
+ );
+ }
+}
+
+export default Confirm;
diff --git a/src/components/sendNew/steps/confirm/confirm.test.js b/src/components/sendNew/steps/confirm/confirm.test.js
new file mode 100644
index 0000000000..4e15f1d1a6
--- /dev/null
+++ b/src/components/sendNew/steps/confirm/confirm.test.js
@@ -0,0 +1,87 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import sinon from 'sinon';
+import configureStore from 'redux-mock-store';
+import PropTypes from 'prop-types';
+
+import accounts from '../../../../../test/constants/accounts';
+import i18n from '../../../../i18n';
+import Confirm from './confirm';
+
+const fakeStore = configureStore();
+
+describe('Confirm Component', () => {
+ let wrapper;
+ const account = accounts.delegate;
+ const props = {
+ account,
+ pendingTransactions: [],
+ passphrase: { value: account.passphrase },
+ secondPassphrase: { value: null },
+ closeDialog: () => {},
+ sent: sinon.spy(),
+ t: key => key,
+ nextStep: () => {},
+ followedAccounts: [],
+ };
+
+ describe('Without account init', () => {
+ beforeEach(() => {
+ account.serverPublicKey = 'public_key';
+ const store = fakeStore({ account });
+
+ wrapper = mount(, {
+ context: { store, i18n },
+ childContextTypes: {
+ store: PropTypes.object.isRequired,
+ i18n: PropTypes.object.isRequired,
+ },
+ });
+ });
+
+ it('renders two Input components', () => {
+ expect(wrapper.find('Input')).to.have.length(3);
+ });
+
+ it('renders two Button component', () => {
+ expect(wrapper.find('Button')).to.have.length(2);
+ });
+
+ it('allows to send a transaction', () => {
+ wrapper.find('.amount input').simulate('change', { target: { value: '120.25' } });
+ wrapper.find('.recipient input').simulate('change', { target: { value: '11004588490103196952L' } });
+ wrapper.find('.send-button button').simulate('click');
+ expect(props.sent).to.have.been.calledWith({
+ account: props.account,
+ amount: '120.25',
+ data: undefined,
+ passphrase: props.account.passphrase,
+ recipientId: '11004588490103196952L',
+ secondPassphrase: null,
+ });
+ });
+ });
+
+ describe('With account init', () => {
+ beforeEach(() => {
+ props.address = '123L';
+ props.amount = 1;
+ props.accountInit = true;
+ const store = fakeStore({ account });
+
+ wrapper = mount(, {
+ context: { store, i18n },
+ childContextTypes: {
+ store: PropTypes.object.isRequired,
+ i18n: PropTypes.object.isRequired,
+ },
+ });
+ });
+
+ it('account initialisation option should not conflict with launch protocol', () => {
+ expect(wrapper.state('recipient').value).to.equal(account.address);
+ expect(wrapper.state('amount').value).to.equal(0.1);
+ });
+ });
+});
diff --git a/src/components/sendNew/steps/confirm/index.js b/src/components/sendNew/steps/confirm/index.js
new file mode 100644
index 0000000000..7034a963e6
--- /dev/null
+++ b/src/components/sendNew/steps/confirm/index.js
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+
+import { sent } from '../../../../actions/transactions';
+import Confirm from './confirm';
+
+const mapStateToProps = state => ({
+ account: state.account,
+ pendingTransactions: state.transactions.pending,
+ failedTransactions: state.transactions.failed,
+ followedAccounts: state.followedAccounts ? state.followedAccounts.accounts : [],
+});
+
+const mapDispatchToProps = dispatch => ({
+ sent: data => dispatch(sent(data)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(translate()(Confirm));
diff --git a/src/components/sendNew/steps/confirm/index.test.js b/src/components/sendNew/steps/confirm/index.test.js
new file mode 100644
index 0000000000..af3cb35a0d
--- /dev/null
+++ b/src/components/sendNew/steps/confirm/index.test.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import i18n from '../../../../i18n';
+import ConfirmHOC from './index';
+
+describe('ConfirmHOC', () => {
+ let wrapper;
+ const store = {};
+ const account = {};
+ const transactions = { pending: [] };
+ beforeEach(() => {
+ store.getState = () => ({
+ account,
+ transactions,
+ });
+ store.subscribe = () => {};
+ store.dispatch = () => {};
+ wrapper = mount();
+ });
+
+ it('should render Send', () => {
+ expect(wrapper.find('Confirm')).to.have.lengthOf(1);
+ });
+
+ it('should mount Send with appropriate properties', () => {
+ const props = wrapper.find('Confirm').props();
+ expect(props.account).to.be.equal(account);
+ expect(typeof props.sent).to.be.equal('function');
+ });
+});
diff --git a/src/components/sendNew/steps/confirm/input.css b/src/components/sendNew/steps/confirm/input.css
new file mode 100644
index 0000000000..02add802c9
--- /dev/null
+++ b/src/components/sendNew/steps/confirm/input.css
@@ -0,0 +1,3 @@
+.input {
+ padding: 0px !important;
+}
diff --git a/src/components/sendNew/steps/form/form.css b/src/components/sendNew/steps/form/form.css
new file mode 100644
index 0000000000..a73b10aeda
--- /dev/null
+++ b/src/components/sendNew/steps/form/form.css
@@ -0,0 +1,154 @@
+@import '../../../app/variables.css';
+
+:root {
+ --tab-inactive-color: var(--color-grayscale-dark);
+ --grid-padding-left: 30px;
+ --header-line-height: 36px;
+ --header-subtitle-font-size: var(--subtitle-font-size);
+ --font-weight-bold: 500;
+ --fee-color: var(--color-grayscale-dark);
+}
+
+.form {
+ position: relative;
+ margin-top: 54px;
+
+ & > div {
+ height: 82px;
+
+ & input {
+ margin-top: 1px;
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+ & > label {
+ font-size: 14px;
+ font-weight: 600px !important;
+ }
+ }
+
+ & input {
+ border-radius: 0;
+ }
+}
+
+.setMaxAmount {
+ position: absolute;
+ right: 0px;
+ bottom: 30px;
+ cursor: pointer;
+ font-size: 14px;
+ color: var(--color-primary-medium);
+ font-weight: var(--font-weight-bold);
+}
+
+.fee {
+ text-align: right;
+ margin-bottom: 24px;
+ font-size: 14px;
+ font-weight: 600;
+ position: absolute;
+ right: 0;
+ top: 70px;
+ color: var(--fee-color);
+}
+
+input:read-only {
+ border-bottom: solid 1px var(--color-white) !important;
+}
+
+.button {
+ min-width: 0;
+ width: 100%;
+}
+
+.accountVisual {
+ position: absolute;
+ left: -40px;
+ top: -15px;
+}
+
+.headerWrapper {
+ padding: 0 !important;
+ margin: 0;
+ line-height: var(--header-line-height);
+
+ & .title {
+ float: left;
+ }
+
+ & .subTitle {
+ font-size: var(--header-subtitle-font-size);
+ }
+
+ & .account {
+ float: right;
+ }
+
+ & h2 {
+ font-weight: var(--font-weight-bold);
+ margin: 0;
+ }
+
+ & h3 {
+ display: inline-block;
+ margin: 0;
+ }
+
+ & .balanceUnit {
+ font-weight: var(--font-weight-bold);
+ }
+
+ & .address {
+ text-align: right;
+ cursor: pointer;
+ }
+
+ & .transfer {
+ color: var(--color-grayscale-dark);
+ }
+}
+
+.sendWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ text-align: left;
+ margin-top: 115px;
+
+ & > footer {
+ width: 100%;
+ margin: 50px 0 0;
+
+ & > button {
+ width: 160px;
+ height: 56px;
+ }
+
+ & > button:last-child {
+ margin-left: 22px;
+ height: 57px;
+ }
+ }
+}
+
+.error {
+ display: block;
+ padding-right: 85px;
+}
+
+@media (--small-viewport) {
+ .headerWrapper {
+ display: none;
+ }
+}
+
+@media (--xSmall-viewport) {
+ .nextButton {
+ width: 100%;
+ }
+}
diff --git a/src/components/sendNew/steps/form/form.js b/src/components/sendNew/steps/form/form.js
new file mode 100644
index 0000000000..089e77ff0e
--- /dev/null
+++ b/src/components/sendNew/steps/form/form.js
@@ -0,0 +1,188 @@
+import React from 'react';
+import { fromRawLsk } from '../../../../utils/lsk';
+import { Button, ActionButton } from '../../../toolbox/buttons/button';
+import { authStatePrefill } from '../../../../utils/form';
+import Converter from '../../../converter';
+import fees from '../../../../constants/fees';
+import styles from './form.css';
+import regex from '../../../../utils/regex';
+import AddressInput from '../../../addressInput';
+import ReferenceInput from '../../../referenceInput';
+import Bookmark from '../../../bookmark';
+
+class Form extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ recipient: {
+ value: this.props.address || '',
+ },
+ amount: {
+ value: this.props.amount || '',
+ },
+ reference: {
+ value: this.props.reference || '',
+ },
+ openFollowedAccountSuggestion: true,
+ ...authStatePrefill(),
+ };
+ this.fee = fees.send;
+ this.inputValidationRegexps = {
+ recipient: regex.address,
+ amount: regex.amount,
+ };
+ }
+
+ componentDidMount() {
+ if (this.props.prevState) {
+ const newState = ['recipient', 'amount', 'reference'].reduce((entries, name) => {
+ const value = this.props.prevState[name] || this.state[name].value;
+ return {
+ ...entries,
+ [name]: {
+ value,
+ error: value ? this.validateInput(name, value, true) : undefined,
+ },
+ };
+ }, {});
+ this.setState({ ...newState, ...authStatePrefill(this.props.account) });
+ }
+ }
+
+ handleChange(name, required = true, value, error) {
+ this.setState({
+ [name]: {
+ value,
+ error: typeof error === 'string' ? error : this.validateInput(name, value, required),
+ },
+ });
+ }
+
+ validateInput(name, value, required) { // eslint-disable-line
+ const byteCount = encodeURI(value).split(/%..|./).length - 1;
+ if (!value && required) {
+ return this.props.t('Required');
+ } else if (name === 'reference' && byteCount > 64) {
+ return this.props.t('Maximum length exceeded');
+ } else if (!value.match(this.inputValidationRegexps[name])) {
+ return name === 'amount' ? this.props.t('Invalid amount') : this.props.t('Invalid address');
+ } else if (name === 'amount' && value > parseFloat(this.getMaxAmount())) {
+ return this.props.t('Not enough LSK');
+ } else if (name === 'amount' && value === '0') {
+ return this.props.t('Zero not allowed');
+ }
+ return undefined;
+ }
+
+ getMaxAmount() {
+ return fromRawLsk(Math.max(0, this.props.account.balance - this.fee));
+ }
+
+ handleSetMaxAmount() {
+ const amount = parseFloat(this.getMaxAmount());
+ this.setState({
+ amount: {
+ value: amount.toString(),
+ },
+ });
+ }
+
+ focusReference() {
+ this.referenceInput.focus();
+ }
+
+ handleFocus() {
+ this.setState({
+ showSetMaxAmount: true,
+ });
+ }
+
+ handleBlur() {
+ /* when click on set max amount link we need a small delay */
+ /* to process the click event before hiding */
+ setTimeout(() => {
+ this.setState({
+ showSetMaxAmount: false,
+ });
+ }, 200);
+ }
+
+ render() {
+ return (
+
+
+ {this.props.t('Send LSK')}
+
+
+
+
+ );
+ }
+}
+
+export default Form;
diff --git a/src/components/sendNew/steps/form/form.test.js b/src/components/sendNew/steps/form/form.test.js
new file mode 100644
index 0000000000..44bfa666d2
--- /dev/null
+++ b/src/components/sendNew/steps/form/form.test.js
@@ -0,0 +1,147 @@
+import React from 'react';
+import { expect } from 'chai';
+import { useFakeTimers } from 'sinon';
+import { mount } from 'enzyme';
+import thunk from 'redux-thunk';
+import configureMockStore from 'redux-mock-store';
+import PropTypes from 'prop-types';
+import accounts from '../../../../../test/constants/accounts';
+import i18n from '../../../../i18n';
+import Form from './form';
+
+describe('Form Component', () => {
+ let wrapper;
+ let props;
+ let clock;
+
+ beforeEach(() => {
+ const account = accounts.delegate;
+
+ clock = useFakeTimers({
+ toFake: ['setTimeout', 'clearTimeout', 'Date', 'setInterval'],
+ });
+
+ const priceTicker = {
+ success: true,
+ LSK: {
+ USD: 1,
+ },
+ };
+
+ const store = configureMockStore([thunk])({
+ account,
+ settings: {},
+ settingsUpdated: () => {},
+ liskService: { priceTicker },
+ });
+
+ props = {
+ account,
+ pendingTransactions: [],
+ closeDialog: () => {},
+ t: key => key,
+ nextStep: () => {},
+ history: { location: { search: '' } },
+ followedAccounts: { accounts: [{ address: '123L', title: 'test' }] },
+ };
+ wrapper = mount(, {
+ context: { store, i18n },
+ childContextTypes: {
+ store: PropTypes.object.isRequired,
+ i18n: PropTypes.object.isRequired,
+ },
+ });
+ });
+
+ afterEach(() => {
+ clock.restore();
+ });
+
+ it('renders three Input components', () => {
+ expect(wrapper.find('Input')).to.have.length(3);
+ });
+
+ it('renders one Button component', () => {
+ expect(wrapper.find('Button')).to.have.length(2);
+ });
+
+ it('accepts valid amount', () => {
+ wrapper.find('.amount input').simulate('change', { target: { value: '120.25' } });
+ expect(wrapper.find('Input.amount').text()).to.not.contain('Invalid amount');
+ });
+
+ it('recognizes invalid amount', () => {
+ wrapper.find('.amount input').simulate('change', { target: { value: '120 INVALID' } });
+ expect(wrapper.find('Input.amount').text()).to.contain('Invalid amount');
+ });
+
+ it('recognizes zero amount', () => {
+ wrapper.find('.amount input').simulate('change', { target: { value: '0' } });
+ expect(wrapper.find('Input.amount').text()).to.contain('Zero not allowed');
+ });
+
+ it('recognizes too high amount', () => {
+ wrapper.find('.amount input').simulate('change', { target: { value: '12000' } });
+ expect(wrapper.find('Input.amount').text()).to.contain('Not enough LSK');
+ });
+
+ it('recognizes empty amount', () => {
+ wrapper.find('.amount input').simulate('change', { target: { value: '12000' } });
+ wrapper.find('.amount input').simulate('change', { target: { value: '' } });
+ expect(wrapper.find('Input.amount').text()).to.contain('Required');
+ });
+
+ it('accepts valid recipient', () => {
+ wrapper.find('.recipient input').simulate('change', { target: { value: '11004588490103196952L' } });
+ expect(wrapper.find('Input.recipient').text()).to.not.contain('Invalid address');
+ });
+
+ it('recognizes invalid recipient', () => {
+ wrapper.find('.recipient input').simulate('change', { target: { value: '11004588490103196952' } });
+ expect(wrapper.find('Input.recipient').text()).to.contain('Invalid address');
+ });
+
+ it('recognizes too big reference length', () => {
+ wrapper.find('.reference input').simulate('change', { target: { value: 'test'.repeat(100) } });
+ expect(wrapper.find('Input.reference').text()).to.contain('Maximum length exceeded');
+ });
+
+ it('displays bookmark', () => {
+ const account = accounts.delegate;
+ const followedAccounts = { accounts: [{ address: '123L', title: '123' }] };
+
+ const store = configureMockStore([thunk])({
+ account,
+ settings: {},
+ settingsUpdated: () => {},
+ followedAccounts,
+ });
+ props.followedAccounts = followedAccounts.accounts;
+ wrapper = mount(, {
+ context: { store, i18n },
+ childContextTypes: {
+ store: PropTypes.object.isRequired,
+ i18n: PropTypes.object.isRequired,
+ },
+ });
+
+ expect(wrapper.find('Bookmark')).to.have.length(1);
+ });
+
+ it('Shows the Set max. amount link on amount focus', () => {
+ wrapper.find('.amount input').simulate('focus');
+ expect(wrapper.state('showSetMaxAmount')).to.equal(true);
+ });
+
+ it('Puts max amount into input field', () => {
+ wrapper.find('.amount input').simulate('focus');
+ wrapper.find('.set-max-amount').simulate('click');
+ expect(wrapper.state('amount').value).to.equal('999.9');
+ });
+
+ it('Hides the Set max. amount link on amount blur', () => {
+ wrapper.find('.amount input').simulate('blur');
+ clock.tick(1200);
+ expect(wrapper.state('showSetMaxAmount')).to.equal(false);
+ });
+});
diff --git a/src/components/sendNew/steps/form/index.js b/src/components/sendNew/steps/form/index.js
new file mode 100644
index 0000000000..b2ff28193c
--- /dev/null
+++ b/src/components/sendNew/steps/form/index.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+
+import Form from './form';
+
+const mapStateToProps = state => ({
+ account: state.account,
+ pendingTransactions: state.transactions.pending,
+ followedAccounts: state.followedAccounts.accounts,
+});
+
+export default connect(mapStateToProps)(translate()(Form));
diff --git a/src/components/sendNew/steps/form/index.test.js b/src/components/sendNew/steps/form/index.test.js
new file mode 100644
index 0000000000..cb117c1028
--- /dev/null
+++ b/src/components/sendNew/steps/form/index.test.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { MemoryRouter as Router } from 'react-router-dom';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import i18n from '../../../../i18n';
+import FormHOC from './index';
+
+describe('FormHOC', () => {
+ let wrapper;
+ const store = {};
+ const peers = {
+ data: {},
+ status: true,
+ };
+ const account = {};
+ const transactions = { pending: [] };
+
+ const priceTicker = {
+ success: true,
+ LSK: {
+ USD: 1,
+ },
+ };
+
+ beforeEach(() => {
+ store.getState = () => ({
+ peers,
+ account,
+ transactions,
+ settings: {},
+ liskService: { priceTicker },
+ followedAccounts: { accounts: [] },
+ });
+ store.subscribe = () => {};
+ store.dispatch = () => {};
+ wrapper = mount(
+
+
+
+ );
+ });
+
+ it('should render Send', () => {
+ expect(wrapper.find('Form')).to.have.lengthOf(1);
+ });
+});
diff --git a/src/components/sendNew/steps/form/stories.js b/src/components/sendNew/steps/form/stories.js
new file mode 100644
index 0000000000..0d19c1db87
--- /dev/null
+++ b/src/components/sendNew/steps/form/stories.js
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { Provider } from 'react-redux';
+import { storiesOf } from '@storybook/react';
+
+import Form from './form';
+
+import accounts from '../../../../../test/constants/accounts';
+import store from '../../../../store';
+
+const account = accounts.genesis;
+
+storiesOf('Form', module)
+ .addDecorator(getStory => (
+
+ {getStory()}
+
+ ))
+ .add('pre-filled recipient and amount', () => (
+
+ ))
+ .add('without pre-filles', () => (
+
+ ));
diff --git a/src/components/sendTo/followAccount.css b/src/components/sendTo/followAccount.css
index 5b29c305f8..7a9bb7a214 100644
--- a/src/components/sendTo/followAccount.css
+++ b/src/components/sendTo/followAccount.css
@@ -4,10 +4,11 @@
line-height: 36px;
display: flex;
justify-content: center;
- background: var(--gradient-greyscale);
+ background: none;
box-shadow: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
+ text-align: left;
& header {
& p {
@@ -31,12 +32,22 @@
& .follow {
margin-top: 25px;
- height: 56px;
+ height: 58px;
& .label {
vertical-align: middle;
}
}
+
+ & > footer {
+ display: flex;
+ justify-content: space-evenly;
+
+ & > button {
+ width: calc(100% / 2 - 20px) !important;
+ margin-top: 0 !important;
+ }
+ }
}
.followedAccountsStep {
diff --git a/src/components/sendTo/followAccount.js b/src/components/sendTo/followAccount.js
index aeb3892377..6b0ef8c7d7 100644
--- a/src/components/sendTo/followAccount.js
+++ b/src/components/sendTo/followAccount.js
@@ -32,7 +32,7 @@ class FollowAccount extends React.Component {
return (
);
diff --git a/src/constants/routes.js b/src/constants/routes.js
index eea395ab56..360227a523 100644
--- a/src/constants/routes.js
+++ b/src/constants/routes.js
@@ -16,6 +16,7 @@ import HwWallet from '../components/hwWallet';
// import NotFound from '../components/notFound';
import AccountVisualDemo from '../components/accountVisual/demo';
import Receive from '../components/receive';
+import Send from '../components/sendNew';
export default {
accountVisualDemo: {
@@ -32,10 +33,15 @@ export default {
path: '/wallet',
component: TransactionDashboard,
isPrivate: true,
+ exact: true,
},
request: {
- path: '/request',
+ path: '/wallet/request',
component: Receive,
+ },
+ send: {
+ path: '/wallet/send',
+ component: Send,
isPrivate: true,
},
delegates: {