Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement On-Boarding module in wallet - Closes #1669 #1734

Merged
merged 9 commits into from Feb 8, 2019
3 changes: 3 additions & 0 deletions i18n/locales/en/common.json
Expand Up @@ -17,6 +17,7 @@
"Add a Lisk ID to follow": "Add a Lisk ID to follow",
"Add a bookmark": "Add a bookmark",
"Add new": "Add new",
"Add some LSK to your Lisk Hub account now!": "Add some LSK to your Lisk Hub account now!",
"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 bookmarks": "Add to bookmarks",
"Add to list": "Add to list",
Expand Down Expand Up @@ -83,6 +84,7 @@
"Connection re-established": "Connection re-established",
"Continue": "Continue",
"Continue to Dashboard": "Continue to Dashboard",
"Copied": "Copied",
"Copied!": "Copied!",
"Copy": "Copy",
"Copy Transaction ID to clipboard": "Copy Transaction ID to clipboard",
Expand Down Expand Up @@ -459,6 +461,7 @@
"You are looking into a saved account. In order to {{nextAction}} you need to enter your passphrase.": "You are looking into a saved account. In order to {{nextAction}} you need to enter your passphrase.",
"You are responsible for safekeeping your second passphrase. No one can restore it, not even Lisk.": "You are responsible for safekeeping your second passphrase. No one can restore it, not even Lisk.",
"You can customize amount & message.": "You can customize amount & message.",
"You can find the LSK token on all of the worlds top exchanges and send them to your unique Lisk address:": "You can find the LSK token on all of the worlds top exchanges and send them to your unique Lisk address:",
"You can now securely manage your LSK tokens.": "You can now securely manage your LSK tokens.",
"You can now use Lisk Hub.<br": {
" If you want to repeat the onboarding, navigate to \"Help\" on the sidebar.": "You can now use Lisk Hub.<br> If you want to repeat the onboarding, navigate to \"Help\" on the sidebar."
Expand Down
39 changes: 39 additions & 0 deletions src/components/toolbox/banner/banner.css
Expand Up @@ -8,6 +8,11 @@
color: var(--color-white);
padding: 40px;

& header {
display: flex;
justify-content: flex-start;
}

& .title {
font-family: var(--heading-font);
font-size: var(--subtitle-font-size);
Expand All @@ -16,6 +21,40 @@
margin: 0;
}

& .closeBtn {
align-items: center;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
box-sizing: border-box;
cursor: pointer;
display: flex;
flex-shrink: 0;
height: 32px;
justify-content: center;
margin-left: auto;
position: relative;
width: 32px;

&::before,
&::after {
background-color: var(--color-white);
border-radius: 1px;
content: '';
height: 15px;
position: absolute;
width: 2px;
}

&::before {
transform: rotate(45deg);
}

&::after {
transform: rotate(-45deg);
}
}

& .content {
font-family: var(--content-font);
font-size: var(--paragraph-font-size-s);
Expand Down
8 changes: 6 additions & 2 deletions src/components/toolbox/banner/banner.js
Expand Up @@ -2,18 +2,22 @@ import React from 'react';
import styles from './banner.css';

const Banner = ({
title, children, footer, className,
title, children, footer, className = '',
onClose,
}) => (
<section className={`${styles.banner} ${className}`}>
<header>
<h1 className={`${styles.title}`}>
{title}
</h1>
{ onClose ? <span
onClick={onClose}
className={`${styles.closeBtn} banner-close`} /> : null}
</header>
<main className={`${styles.content}`}>
{ children }
</main>
<footer className={`${styles.footer}`}>
<footer>
{ footer }
</footer>
</section>
Expand Down
10 changes: 10 additions & 0 deletions src/components/toolbox/banner/banner.test.js
@@ -1,6 +1,7 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import { spy } from 'sinon';
import Banner from './banner';

describe('Banner', () => {
Expand All @@ -21,4 +22,13 @@ describe('Banner', () => {
expect(wrapper.find('main')).to.contain(props.children);
expect(wrapper.find('footer')).to.contain(props.footer);
});

it('Should render with close button and call onClose props', () => {
const newProps = { onClose: spy() };
wrapper.setProps(newProps);
wrapper.update();
expect(wrapper).to.have.descendants('.closeBtn');
wrapper.find('.closeBtn').simulate('click');
expect(newProps.onClose).to.have.been.calledWith();
});
});
16 changes: 0 additions & 16 deletions src/components/transactionsV2/walletTransactionsV2/index.js
Expand Up @@ -3,8 +3,6 @@ import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import { withRouter } from 'react-router-dom';
import { transactionsRequested, transactionsFilterSet } from '../../../actions/transactions';
import { accountVotersFetched, accountVotesFetched } from '../../../actions/account';
import { searchAccount } from '../../../actions/search';
import WalletTransactionsV2 from './walletTransactionsV2';
import actionTypes from '../../../constants/actions';
import txFilters from './../../../constants/transactionFilters';
Expand All @@ -18,29 +16,15 @@ const mapStateToProps = state => ({
state.transactions.pending,
state.transactions.confirmed,
),
transactionsCount: state.transactions.count,
votes: state.account.votes ?
state.account.votes :
state.search.votes[state.account.addres],
voters: state.account.voters ?
state.account.voters :
state.search.voters[state.account.address],
count: state.transactions.count,
// Pick delegate from source
delegate: (state.account && state.account.delegate) ?
state.account && (state.account.delegate || null) :
state.search.delegates[state.account.address],
activeFilter: state.filters.wallet || txFilters.all,
loading: state.loading,
followedAccounts: state.followedAccounts.accounts,
});

const mapDispatchToProps = {
searchAccount,
transactionsRequested,
transactionsFilterSet,
accountVotersFetched,
accountVotesFetched,
addFilter: data => ({ type: actionTypes.addFilter, data }),
};

Expand Down
@@ -0,0 +1,32 @@
@import '../../app/variablesV2.css';

.onboarding {
margin-bottom: 24px;

& .copyAddress {
display: flex;
flex-wrap: wrap;
margin-top: 40px;

& .address {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
box-sizing: border-box;
color: var(--color-white);
font-family: var(--content-font);
font-size: var(--paragraph-font-size-s);
font-weight: var(--font-weight-bold);
letter-spacing: 0.3px;
line-height: 1.07;
margin-right: 24px;
outline: none;
padding: 16px 24px;
text-align: center;
}

& > button {
width: 116px;
}
}
}
@@ -1,17 +1,34 @@
import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { SecondaryButtonV2 } from '../../toolbox/buttons/button';
import localJSONStorage from '../../../utils/localJSONStorage';
import TransactionsOverviewV2 from '../transactionsOverviewV2';
import txFilters from '../../../constants/transactionFilters';
import Banner from '../../toolbox/banner/banner';
import WalletHeader from './walletHeader';
import routes from '../../../constants/routes';
import styles from './walletTransactionsV2.css';

class WalletTransactionsV2 extends React.Component {
constructor() {
super();

this.state = {
copied: false,
closedOnboarding: false,
};
this.copyTimeout = null;

this.onInit = this.onInit.bind(this);
this.onLoadMore = this.onLoadMore.bind(this);
this.onFilterSet = this.onFilterSet.bind(this);
this.onTransactionRowClick = this.onTransactionRowClick.bind(this);
this.onCopy = this.onCopy.bind(this);
this.closeOnboarding = this.closeOnboarding.bind(this);
}

componentWillUnmount() {
clearTimeout(this.copyTimeout);
}

onInit() {
Expand All @@ -21,14 +38,6 @@ class WalletTransactionsV2 extends React.Component {
filter: txFilters.all,
});

this.props.searchAccount({
address: this.props.address,
});

this.props.accountVotesFetched({
address: this.props.address,
});

this.props.addFilter({
filterName: 'wallet',
value: txFilters.all,
Expand Down Expand Up @@ -69,19 +78,58 @@ class WalletTransactionsV2 extends React.Component {
this.props.history.push(transactionPath);
}

onCopy() {
clearTimeout(this.copyTimeout);
this.setState({
copied: true,
});

this.copyTimeout = setTimeout(() =>
this.setState({
copied: false,
}), 3000);
}

closeOnboarding() {
localJSONStorage.set('closedWalletOnboarding', 'true');
this.setState({ closedOnboarding: true });
}

render() {
const overviewProps = {
...this.props,
canLoadMore: this.props.transactions.length < this.props.transactionsCount,
canLoadMore: this.props.transactions.length < this.props.count,
onInit: this.onInit,
onLoadMore: this.onLoadMore,
onFilterSet: this.onFilterSet,
onTransactionRowClick: this.onTransactionRowClick,
};

const { t, account } = this.props;

return (
<React.Fragment>
<WalletHeader {...this.props} />
{ account.balance === 0 && localJSONStorage.get('closedWalletOnboarding') !== 'true' ?
<Banner
className={`${styles.onboarding} wallet-onboarding`}
onClose={this.closeOnboarding}
title={t('Add some LSK to your Lisk Hub account now!')}
footer={(
<div className={styles.copyAddress}>
<span className={styles.address}>{account.address}</span>
<CopyToClipboard
text={account.address}
onCopy={this.onCopy}>
<SecondaryButtonV2 disabled={this.state.copied}>
<span>{this.state.copied ? t('Copied') : t('Copy')}</span>
</SecondaryButtonV2>
</CopyToClipboard>
</div>
)}>
<p>{t('You can find the LSK token on all of the worlds top exchanges and send them to your unique Lisk address:')}</p>
</Banner> : null
}
<TransactionsOverviewV2 {...overviewProps} />
</React.Fragment>
);
Expand Down
@@ -1,5 +1,5 @@
import React from 'react';
import { spy } from 'sinon';
import { spy, useFakeTimers } from 'sinon';
import { mount } from 'enzyme';
import { expect } from 'chai';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -59,11 +59,8 @@ describe('WalletTransactions V2 Component', () => {
followedAccounts: [],
transactionsCount: 1000,
transactions,
searchAccount: spy(),
transactionsRequested: spy(),
transactionsFilterSet: spy(),
accountVotersFetched: spy(),
accountVotesFetched: spy(),
addFilter: spy(),
loading: [],
t: key => key,
Expand All @@ -85,4 +82,43 @@ describe('WalletTransactions V2 Component', () => {
wrapper.find('.transactions-row').first().simulate('click');
expect(props.history.push).to.have.been.calledWith(transactionPath);
});

describe('Onboarding Banner', () => {
let clock;

beforeEach(() => {
const newProps = {
...props,
account: accounts['empty account'],
};

clock = useFakeTimers({
now: new Date(2018, 1, 1),
toFake: ['setTimeout', 'clearTimeout'],
});

wrapper = mount(<Router>
<WalletTransactionsV2 {...newProps} />
</Router>, options);
});

afterEach(() => {
clock.restore();
});

it('should copy address on click on banner copy and setTimeout', () => {
const copyBtn = wrapper.find('.onboarding .copyAddress button');
expect(copyBtn).to.have.text('Copy');
copyBtn.simulate('click');
expect(copyBtn).to.have.text('Copied');
clock.tick(3000);
expect(copyBtn).to.have.text('Copy');
});

it('should render onboarding banner and hide when close is clicked', () => {
expect(wrapper).to.have.descendants('.onboarding');
wrapper.find('.onboarding .closeBtn').simulate('click');
expect(wrapper).to.not.have.descendants('.onboarding');
});
});
});
2 changes: 2 additions & 0 deletions test/constants/selectors.js
Expand Up @@ -168,6 +168,8 @@ const ss = {
passphraseWordConfirm: '.passphrase-holder .word-option',
passphraseConfirmButton: '.passphrase-is-correct-button',
exploreAsGuestBtn: '.explore-as-guest-button',
walletOnboarding: '.wallet-onboarding',
walletOnboardingClose: '.wallet-onboarding .banner-close',
};

export default ss;
15 changes: 15 additions & 0 deletions test/cypress/e2e/wallet.spec.js
Expand Up @@ -16,6 +16,21 @@ describe('Wallet', () => {
cy.get(ss.transactionSendButton);
});

/**
* On boarding banner shows up if balance is 0 and localStorage.closedWalletOnboarding not set
* @expect balance is 0
* @expect On boarding banner is present on it
* @expect After clicking close doesn't show banner again
*/
it('Wallet page shows onboarding banner', () => {
cy.autologin(accounts['empty account'].passphrase, networks.devnet.node);
cy.visit(urls.wallet);
cy.get(ss.walletOnboarding).should('exist');
cy.get(ss.walletOnboardingClose).click();
cy.reload();
cy.get(ss.walletOnboarding).should('not.exist');
});

/**
* Sidebar link leads to Wallet page
* @expect url is correct
Expand Down