Skip to content

Commit

Permalink
feat(account): added transaction history section (#752)
Browse files Browse the repository at this point in the history
* feat(account): added transaction panel

* chore(tx): testing

* chore(account): updated query

* chore(account): code cleanup

* chore(store): removed default store items

* chore(account): replaced static with dynamic content and fixed css

* chore(account): linting fixes

* chore(root): updated renovate config

* chore(account): prepare for infinite scroll

* chore(account): fixed pagination

* chore(account): linting fix

* chore(account): fixed token issue and modified scrollbar

* chore(tests): added test

* chore(account): Tabs CSS Fixes

- Fix alignment of "send" "received" text
- Fix Send/Receive/Transaction tab spacing (it's uneven)
- Make default icon colors + sizes consistent with design

* chore(txPanel): removed selectability of tabs

* chore(transactionsTab): added empty state, fixed render/css issues

* chore(tests): update tests

* chore(txHistory): fix prop validation

* chore(txHIstory): fix css max height

* chore(tx): DRY + Improve label styles

* chore(tx): Style Tweaks + alignment with design

* chore(txHistory): fixed stylelint issue
  • Loading branch information
Maurice Dalderup committed Dec 30, 2018
1 parent 1196f1b commit c3fe50e
Show file tree
Hide file tree
Showing 23 changed files with 436 additions and 34 deletions.
@@ -0,0 +1,25 @@
import React from 'react';
import { mount } from 'enzyme';

import { provideStore, createStore, spunkyKey, mockSpunkyLoaded } from 'testHelpers';

import Transactions from 'account/components/TransactionsPanel/Transactions';

const address = 'ALfnhLg7rUyL6Jr98bzzoxz5J7m64fbR4s';

const initialState = {
[spunkyKey]: {
auth: mockSpunkyLoaded({ address })
}
};

const mountContainer = (props = {}) => {
return mount(provideStore(<Transactions {...props} />, createStore(initialState)));
};

describe('<Transactions />', () => {
it('renders the transactions panel', () => {
const wrapper = mountContainer();
expect(wrapper.find(Transactions).exists()).toBe(true);
});
});
8 changes: 6 additions & 2 deletions renovate.json
Expand Up @@ -30,8 +30,8 @@
"allowedVersions": "< 3"
},
{
"packagePatterns": ["eslint"],
"groupName": "eslint"
"packagePatterns": ["eslint", "stylelint"],
"groupName": "lint"
},
{
"packagePatterns": ["fortawesome"],
Expand All @@ -40,6 +40,10 @@
{
"packagePatterns": ["babel"],
"groupName": "babel"
},
{
"packagePatterns": ["enzyme"],
"groupName": "enzyme"
}
]
}
21 changes: 21 additions & 0 deletions src/common/stylesheets/mixins.scss
Expand Up @@ -101,3 +101,24 @@
@mixin fade-out {
animation: fade-out 0.25s linear;
}

::-webkit-scrollbar {
width: 4px;
height: 4px;
}

/* Track */
::-webkit-scrollbar-track {
background: none;
}

/* Handle */
::-webkit-scrollbar-thumb {
border-radius: 25px;
background: #9a99a5;
}

/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #494759;
}
87 changes: 87 additions & 0 deletions src/renderer/account/actions/transactionHistoryActions.js
@@ -0,0 +1,87 @@
import fetch from 'node-fetch';
import uuid from 'uuid/v4';
import { createActions } from 'spunky';
import { omit } from 'lodash';

import { api } from '@cityofzion/neon-js';

import { NEO, GAS, ASSETS } from 'shared/values/assets';
import getTokens from 'shared/util/getTokens';

const TX_TYPES = {
SEND: 'Send',
RECEIVE: 'Receive',
CLAIM: 'Claim',
INVOCATION: 'Invocation'
};

function parseAbstractData(data, currentUserAddress, tokens) {
const parsedTxType = (abstract) => {
if (abstract.address_to === currentUserAddress && abstract.address_from !== 'claim') {
return TX_TYPES.RECEIVE;
}
if (abstract.address_from === 'claim') return TX_TYPES.CLAIM;
return TX_TYPES.SEND;
};

const parsedAsset = (abstract) => {
const tokensResult = tokens.find(({ scriptHash }) => scriptHash === abstract.asset);
if (tokensResult) return tokensResult;
if (abstract.asset === NEO || abstract.asset === GAS) {
return { ...ASSETS[abstract.asset], scriptHash: abstract.asset };
}
return { symbol: '' };
};

const parsedTo = (abstract) => {
if (abstract.address_to === 'fees') return 'NETWORK FEES';
if (abstract.address_to === 'mint') return 'MINT TOKENS';
return abstract.address_to;
};

const parsedFrom = (abstract) => {
if (abstract.address_from === 'mint') return 'MINT TOKENS';
return abstract.address_from;
};

return data.map((abstract) => {
const asset = parsedAsset(abstract);
const type = parsedTxType(abstract);
const summary = {
to: parsedTo(abstract),
isNetworkFee: abstract.address_to === 'fees',
from: parsedFrom(abstract),
txId: abstract.txid,
time: abstract.time,
amount: abstract.amount,
asset,
label: abstract.address_to === currentUserAddress ? 'IN' : 'OUT',
type,
id: uuid()
};

return summary;
});
}

async function getTransactionHistory({ net, address, previousCall = {} }) {
const { page_number: pageNumber = 0, total_pages: totalPages = 1, entries = [] } = previousCall;
const pageCount = pageNumber + 1;
if (pageCount > totalPages) return previousCall;

const tokens = await getTokens();
const endpoint = api.neoscan.getAPIEndpoint(net);

const data = await fetch(`${endpoint}/v1/get_address_abstracts/${address}/${pageCount}`);
const response = await data.json();
return {
...omit(response, 'entries'),
entries: [...entries, ...parseAbstractData(response.entries, address, tokens)]
};
}

export const ID = 'transaction_history';

export default createActions(ID, ({ net, address, previousCall }) => async () => {
return getTransactionHistory({ net, address, previousCall });
});
Expand Up @@ -4,11 +4,11 @@
align-items: center;

.icon {
flex: 0 0 auto;
margin-bottom: 8px;
}

.label {
flex: 0 0 auto;
user-select: none;
}
}
@@ -0,0 +1,46 @@
import React from 'react';
import classNames from 'classnames';
import { string } from 'prop-types';

import ExplorerLink from 'root/components/AuthenticatedLayout/ExplorerLink';

import transactionShape from '../../../../shapes/transactionShape';

import styles from './Transaction.scss';

export default class Transaction extends React.PureComponent {
static propTypes = {
className: string,
address: string.isRequired,
transaction: transactionShape.isRequired
};

static defaultProps = {
className: null
};

render() {
const {
className,
address,
transaction: { to, amount, label, asset, txId, type }
} = this.props;

const labelStyle = to === address ? styles.labelIn : styles.labelOut;

return (
<div className={classNames(styles.transaction, className)}>
<div className={styles.transactionInfoWrap}>
<div className={classNames(labelStyle, styles.label)}> {label} </div>
<div className={styles.type}> {type} </div>
<div className={styles.asset}>
{amount} {asset.symbol}
</div>
</div>
<ExplorerLink endpoint={`transaction/${txId}`} className={styles.transactionId}>
{txId}
</ExplorerLink>
</div>
);
}
}
@@ -0,0 +1,70 @@
.transaction {
display: flex;
flex-wrap: wrap;
padding: 8px;
margin-top: 10px;
margin-bottom: 10px;
background-color: #f8f9fa;
user-select: none;
cursor: default;

.transactionInfoWrap {
display: flex;
align-items: flex-start;
align-items: center;
width: 100%;
}

.asset {
color: #353445;
line-height: 21px;
text-align: right;
font-size: 14px;
flex: 1 1 auto;
margin-left: auto;
}

.type {
color: #353445;
font-weight: 500;
line-height: 23px;
text-align: left;
font-size: 14px;
margin-left: 10px;
flex: 0 1 auto;
}

.label {
border-radius: 2px;
font-size: 9px;
line-height: 13px;
color: white;
padding-left: 4px;
padding-right: 4px;
font-weight: 500;
flex: 0 1 auto;
min-width: 28px;

&In {
background-color: #5ebb46;
}

&Out {
background-color: red;
}
}

.transactionId {
color: #5ebb46;
padding: 2px 0 0 0;
line-height: 18px;
text-align: left;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
user-select: text;
&:hover {
background: none;
}
}
}
@@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { openTab } from 'browser/actions/browserActions';

import Transaction from './Transaction';

const mapDispatchToProps = (dispatch) => bindActionCreators(
{
onOpen: (target) => openTab(target && { target })
},
dispatch
);

export default connect(
null,
mapDispatchToProps
)(Transaction);
@@ -0,0 +1,47 @@
import React from 'react';
import classNames from 'classnames';
import { string, func } from 'prop-types';
import { isEmpty } from 'lodash';

import Transaction from './Transaction';
import transactionHistoryShape from '../../../shapes/transactionHistoryShape';

import styles from './Transactions.scss';

export default class Transactions extends React.PureComponent {
static propTypes = {
className: string,
address: string.isRequired,
transactionHistory: transactionHistoryShape.isRequired,
handleFetchAdditionalTxData: func.isRequired
};

static defaultProps = {
className: null
};

render() {
const { className, transactionHistory, address } = this.props;

return (
<div className={classNames(styles.transactions, className)} onScroll={this.handleScroll}>
{this.renderTransactions(transactionHistory, address)}
</div>
);
}

renderTransactions = ({ entries }, address) => {
if (isEmpty(entries)) {
return <div> No transaction history found. </div>;
}
return entries.map((tx) => <Transaction key={tx.id} transaction={tx} address={address} />);
};

handleScroll = ({ target }) => {
const { handleFetchAdditionalTxData, transactionHistory } = this.props;
const bottom = target.scrollHeight - target.scrollTop === target.clientHeight;
if (bottom) {
handleFetchAdditionalTxData(transactionHistory);
}
};
}
@@ -0,0 +1,6 @@
.transactions {
margin: 20px 0 0;
text-align: center;
max-height: 475px;
overflow-y: scroll;
}

0 comments on commit c3fe50e

Please sign in to comment.