Wallet progress

This commit is contained in:
6ea86b96 2017-04-22 20:17:01 +07:00 committed by Jeremy Kauffman
parent cee2f2a626
commit 089572880f
21 changed files with 629 additions and 394 deletions

View file

@ -52,15 +52,6 @@ export function doCloseModal() {
}
}
export function doUpdateBalance(balance) {
return {
type: types.UPDATE_BALANCE,
data: {
balance: balance
}
}
}
export function doUpdateDownloadProgress(percent) {
return {
type: types.UPGRADE_DOWNLOAD_PROGRESSED,
@ -204,3 +195,9 @@ export function doSearch(term) {
})
}
}
export function doDaemonReady() {
return {
type: types.DAEMON_READY
}
}

60
ui/js/actions/wallet.js Normal file
View file

@ -0,0 +1,60 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
export function doUpdateBalance(balance) {
return {
type: types.UPDATE_BALANCE,
data: {
balance: balance
}
}
}
export function doFetchTransactions() {
return function(dispatch, getState) {
dispatch({
type: types.FETCH_TRANSACTIONS_STARTED
})
lbry.call('get_transaction_history', {}, (results) => {
dispatch({
type: types.FETCH_TRANSACTIONS_COMPLETED,
data: {
transactions: results
}
})
})
}
}
export function doGetNewAddress() {
return function(dispatch, getState) {
dispatch({
type: types.GET_NEW_ADDRESS_STARTED
})
lbry.wallet_new_address().then(function(address) {
localStorage.setItem('wallet_address', address);
dispatch({
type: types.GET_NEW_ADDRESS_COMPLETED,
data: { address }
})
})
}
}
export function doCheckAddressIsMine(address) {
return function(dispatch, getState) {
dispatch({
type: types.CHECK_ADDRESS_IS_MINE_STARTED
})
lbry.checkAddressIsMine(address, (isMine) => {
if (!isMine) dispatch(doGetNewAddress())
dispatch({
type: types.CHECK_ADDRESS_IS_MINE_COMPLETED
})
})
}
}

View file

@ -2,8 +2,7 @@ import React from 'react'
import lbry from 'lbry.js';
import Router from 'component/router'
import Drawer from 'component/drawer';
import Header from 'component/header.js';
import Header from 'component/header';
import {Modal, ExpandableModal} from 'component/modal.js';
import ErrorModal from 'component/errorModal'
import DownloadingModal from 'component/downloadingModal'

View file

@ -7,12 +7,16 @@ import {
doNavigate,
doCloseDrawer,
doLogoClick,
doUpdateBalance,
} from 'actions/app'
import {
doUpdateBalance,
} from 'actions/wallet'
import {
selectCurrentPage,
selectBalance,
} from 'selectors/app'
import {
selectBalance,
} from 'selectors/wallet'
const select = (state) => ({
currentPage: selectCurrentPage(state),

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
selectCurrentPage,
selectHeaderLinks,
} from 'selectors/app'
import {
doNavigate,
} from 'actions/app'
import Header from './view'
const select = (state) => ({
currentPage: selectCurrentPage(state),
subLinks: selectHeaderLinks(state),
})
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
})
export default connect(select, perform)(Header)

View file

@ -3,7 +3,7 @@ import lbryuri from '../lbryuri.js';
import {Icon, CreditAmount} from './common.js';
import Link from 'component/link';
var Header = React.createClass({
let Header = React.createClass({
_balanceSubscribeId: null,
_isMounted: false,
@ -190,24 +190,4 @@ class WunderBar extends React.PureComponent {
}
}
export let SubHeader = React.createClass({
render: function() {
let links = [],
viewingUrl = '?' + this.props.viewingPage;
for (let link of Object.keys(this.props.links)) {
links.push(
<a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }>
{this.props.links[link]}
</a>
);
}
return (
<nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
{links}
</nav>
);
}
});
export default Header;

View file

@ -4,9 +4,7 @@ import HelpPage from 'page/help';
import WatchPage from 'page/watch.js';
import ReportPage from 'page/report.js';
import StartPage from 'page/start.js';
import ClaimCodePage from 'page/claim_code.js';
import ReferralPage from 'page/referral.js';
import WalletPage from 'page/wallet.js';
import WalletPage from 'page/wallet';
import DetailPage from 'page/show.js';
import PublishPage from 'page/publish.js';
import DiscoverPage from 'page/discover.js';

View file

@ -0,0 +1,22 @@
const SubHeader = (props) => {
const {
subLinks,
currentPage,
navigate,
} = props
const links = [],
viewingUrl = '?' + this.props.viewingPage;
for(let link of Object.keys(subLinks)) {
links.push(
<a href="#" onClick={() => navigate(link)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }>
{subLinks[link]}
</a>
);
}
return (
<nav className={"sub-header" + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>{links}</nav>
)
}

View file

@ -0,0 +1,12 @@
import {SubHeader} from '../component/sub-header.js';
export let WalletNav = React.createClass({
render: function () {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?wallet': 'Overview',
'?send': 'Send',
'?receive': 'Receive',
'?rewards': 'Rewards'
}} />;
}
});

View file

@ -1,5 +1,13 @@
export const UPDATE_BALANCE = 'UPDATE_BALANCE'
export const NAVIGATE = 'NAVIGATE'
export const OPEN_MODAL = 'OPEN_MODAL'
export const CLOSE_MODAL = 'CLOSE_MODAL'
export const OPEN_DRAWER = 'OPEN_DRAWER'
export const CLOSE_DRAWER = 'CLOSE_DRAWER'
export const START_SEARCH = 'START_SEARCH'
export const DAEMON_READY = 'DAEMON_READY'
// Upgrades
export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED'
@ -12,10 +20,11 @@ export const UPDATE_VERSION = 'UPDATE_VERSION'
export const SKIP_UPGRADE = 'SKIP_UPGRADE'
export const START_UPGRADE = 'START_UPGRADE'
export const OPEN_MODAL = 'OPEN_MODAL'
export const CLOSE_MODAL = 'CLOSE_MODAL'
export const OPEN_DRAWER = 'OPEN_DRAWER'
export const CLOSE_DRAWER = 'CLOSE_DRAWER'
export const START_SEARCH = 'START_SEARCH'
// Wallet
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED'
export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED'
export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED'
export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED'
export const UPDATE_BALANCE = 'UPDATE_BALANCE'
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED'
export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED'

View file

@ -9,6 +9,7 @@ import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js';
import { Provider } from 'react-redux';
import store from 'store.js';
import { runTriggers } from 'triggers'
const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu');
@ -23,6 +24,8 @@ window.addEventListener('contextmenu', (event) => {
});
const initialState = app.store.getState();
app.store.subscribe(runTriggers);
runTriggers();
var init = function() {
window.lbry = lbry;
@ -35,7 +38,7 @@ var init = function() {
function onDaemonReady() {
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
ReactDOM.render(<Provider store={store}><div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div></Provider>, canvas)
ReactDOM.render(<Provider store={store}><div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div></Provider>, canvas)
}
if (window.sessionStorage.getItem('loaded') == 'y') {

View file

@ -48,7 +48,7 @@ var HelpPage = React.createClass({
}
return (
<main className="page">
<main className="main--single-column">
<section className="card">
<h3>Read the FAQ</h3>
<p>Our FAQ answers many common questions.</p>

View file

@ -1,326 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import Link from 'component/link';
import Modal from '../component/modal.js';
import {SubHeader} from '../component/header.js';
import {FormField, FormRow} from '../component/form.js';
import {Address, BusyMessage, CreditAmount} from '../component/common.js';
var AddressSection = React.createClass({
_refreshAddress: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
lbry.getUnusedAddress((address) => {
window.localStorage.setItem('wallet_address', address);
this.setState({
address: address,
});
});
},
_getNewAddress: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
lbry.getNewAddress((address) => {
window.localStorage.setItem('wallet_address', address);
this.setState({
address: address,
});
});
},
getInitialState: function() {
return {
address: null,
modal: null,
}
},
componentWillMount: function() {
var address = window.localStorage.getItem('wallet_address');
if (address === null) {
this._refreshAddress();
} else {
lbry.checkAddressIsMine(address, (isMine) => {
if (isMine) {
this.setState({
address: address,
});
} else {
this._refreshAddress();
}
});
}
},
render: function() {
return (
<section className="card">
<div className="card__title-primary">
<h3>Wallet Address</h3>
</div>
<div className="card__content">
<Address address={this.state.address} />
</div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={this._getNewAddress} />
</div>
<div className="card__content">
<div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
</div>
</div>
</section>
);
}
});
var SendToAddressSection = React.createClass({
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if ((this.state.balance - this.state.amount) < 1)
{
this.setState({
modal: 'insufficientBalance',
});
return;
}
this.setState({
results: "",
});
lbry.sendToAddress(this.state.amount, this.state.address, (results) => {
if(results === true)
{
this.setState({
results: "Your transaction was successfully placed in the queue.",
});
}
else
{
this.setState({
results: "Something went wrong: " + results
});
}
}, (error) => {
this.setState({
results: "Something went wrong: " + error.message
})
});
},
closeModal: function() {
this.setState({
modal: null,
});
},
getInitialState: function() {
return {
address: "",
amount: 0.0,
balance: <BusyMessage message="Checking balance" />,
results: "",
}
},
componentWillMount: function() {
lbry.getBalance((results) => {
this.setState({
balance: results,
});
});
},
setAmount: function(event) {
this.setState({
amount: parseFloat(event.target.value),
})
},
setAddress: function(event) {
this.setState({
address: event.target.value,
})
},
render: function() {
return (
<section className="card">
<form onSubmit={this.handleSubmit}>
<div className="card__title-primary">
<h3>Send Credits</h3>
</div>
<div className="card__content">
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
</div>
<div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
<input type='submit' className='hidden' />
</div>
{
this.state.results ?
<div className="card__content">
<h4>Results</h4>
{this.state.results}
</div> : ''
}
</form>
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
onConfirmed={this.closeModal}>
Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
</Modal>
</section>
);
}
});
var TransactionList = React.createClass({
getInitialState: function() {
return {
transactionItems: null,
}
},
componentWillMount: function() {
lbry.call('get_transaction_history', {}, (results) => {
if (results.length == 0) {
this.setState({ transactionItems: [] })
} else {
var transactionItems = [],
condensedTransactions = {};
results.forEach(function(tx) {
var txid = tx["txid"];
if (!(txid in condensedTransactions)) {
condensedTransactions[txid] = 0;
}
condensedTransactions[txid] += parseFloat(tx["value"]);
});
results.reverse().forEach(function(tx) {
var txid = tx["txid"];
if (condensedTransactions[txid] && condensedTransactions[txid] != 0)
{
transactionItems.push({
id: txid,
date: tx["timestamp"] ? (new Date(parseInt(tx["timestamp"]) * 1000)) : null,
amount: condensedTransactions[txid]
});
delete condensedTransactions[txid];
}
});
this.setState({ transactionItems: transactionItems });
}
});
},
render: function() {
var rows = [];
if (this.state.transactionItems && this.state.transactionItems.length > 0)
{
this.state.transactionItems.forEach(function(item) {
rows.push(
<tr key={item.id}>
<td>{ (item.amount > 0 ? '+' : '' ) + item.amount }</td>
<td>{ item.date ? item.date.toLocaleDateString() : <span className="empty">(Transaction pending)</span> }</td>
<td>{ item.date ? item.date.toLocaleTimeString() : <span className="empty">(Transaction pending)</span> }</td>
<td>
<a className="button-text" href={"https://explorer.lbry.io/tx/"+item.id} target="_blank">{item.id.substr(0, 7)}</a>
</td>
</tr>
);
});
}
return (
<section className="card">
<div className="card__title-primary">
<h3>Transaction History</h3>
</div>
<div className="card__content">
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
{ this.state.transactionItems && rows.length > 0 ?
<table className="table-standard table-stretch">
<thead>
<tr>
<th>Amount</th>
<th>Date</th>
<th>Time</th>
<th>Transaction</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
: ''
}
</div>
</section>
);
}
});
export let WalletNav = React.createClass({
render: function() {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?wallet': 'Overview',
'?send': 'Send',
'?receive': 'Receive',
'?rewards': 'Rewards'
}} />;
}
});
var WalletPage = React.createClass({
_balanceSubscribeId: null,
propTypes: {
viewingPage: React.PropTypes.string,
},
/*
Below should be refactored so that balance is shared all of wallet page. Or even broader?
What is the proper React pattern for sharing a global state like balance?
*/
getInitialState: function() {
return {
balance: null,
}
},
componentWillMount: function() {
this._balanceSubscribeId = lbry.balanceSubscribe((results) => {
this.setState({
balance: results,
})
});
},
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId);
}
},
render: function() {
return (
<main className="main--single-column">
<WalletNav viewingPage={this.props.viewingPage} />
<section className="card">
<div className="card__title-primary">
<h3>Balance</h3>
</div>
<div className="card__content">
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
</div>
</section>
{ this.props.viewingPage === 'wallet' ? <TransactionList /> : '' }
{ this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' }
{ this.props.viewingPage === 'receive' ? <AddressSection /> : '' }
</main>
);
}
});
export default WalletPage;

View file

@ -0,0 +1,39 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doCloseModal,
} from 'actions/app'
import {
doGetNewAddress,
} from 'actions/wallet'
import {
selectCurrentPage,
} from 'selectors/app'
import {
selectBalance,
selectTransactions,
selectTransactionItems,
selectIsFetchingTransactions,
selectReceiveAddress,
selectGettingNewAddress,
} from 'selectors/wallet'
import WalletPage from './view'
const select = (state) => ({
currentPage: selectCurrentPage(state),
balance: selectBalance(state),
transactions: selectTransactions(state),
fetchingTransactions: selectIsFetchingTransactions(state),
transactionItems: selectTransactionItems(state),
receiveAddress: selectReceiveAddress(state),
gettingNewAddress: selectGettingNewAddress(state),
})
const perform = (dispatch) => ({
closeModal: () => dispatch(doCloseModal()),
getNewAddress: () => dispatch(doGetNewAddress()),
})
export default connect(select, perform)(WalletPage)

219
ui/js/page/wallet/view.jsx Normal file
View file

@ -0,0 +1,219 @@
import React from 'react';
import lbry from 'lbry.js';
import Link from 'component/link';
import Modal from 'component/modal';
import {
FormField,
FormRow
} from 'component/form';
import {
Address,
BusyMessage,
CreditAmount
} from 'component/common';
const AddressSection = (props) => {
const {
receiveAddress,
getNewAddress,
gettingNewAddress,
} = props
return (
<section className="card">
<div className="card__title-primary">
<h3>Wallet Address</h3>
</div>
<div className="card__content">
<Address address={receiveAddress} />
</div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={getNewAddress} disabled={gettingNewAddress}/>
</div>
<div className="card__content">
<div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
</div>
</div>
</section>
);
}
var SendToAddressSection = React.createClass({
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if ((this.state.balance - this.state.amount) < 1)
{
this.setState({
modal: 'insufficientBalance',
});
return;
}
this.setState({
results: "",
});
lbry.sendToAddress(this.state.amount, this.state.address, (results) => {
if(results === true)
{
this.setState({
results: "Your transaction was successfully placed in the queue.",
});
}
else
{
this.setState({
results: "Something went wrong: " + results
});
}
}, (error) => {
this.setState({
results: "Something went wrong: " + error.message
})
});
},
closeModal: function() {
this.setState({
modal: null,
});
},
getInitialState: function() {
return {
address: "",
amount: 0.0,
balance: <BusyMessage message="Checking balance" />,
results: "",
}
},
componentWillMount: function() {
lbry.getBalance((results) => {
this.setState({
balance: results,
});
});
},
setAmount: function(event) {
this.setState({
amount: parseFloat(event.target.value),
})
},
setAddress: function(event) {
this.setState({
address: event.target.value,
})
},
render: function() {
return (
<section className="card">
<form onSubmit={this.handleSubmit}>
<div className="card__title-primary">
<h3>Send Credits</h3>
</div>
<div className="card__content">
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
</div>
<div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
<input type='submit' className='hidden' />
</div>
{
this.state.results ?
<div className="card__content">
<h4>Results</h4>
{this.state.results}
</div> : ''
}
</form>
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
onConfirmed={this.closeModal}>
Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
</Modal>
</section>
);
}
});
const TransactionList = (props) => {
const {
transactions,
fetchingTransactions,
transactionItems,
} = props
const rows = []
if (transactions.length > 0) {
transactionItems.forEach(function(item) {
rows.push(
<tr key={item.id}>
<td>{ (item.amount > 0 ? '+' : '' ) + item.amount }</td>
<td>{ item.date ? item.date.toLocaleDateString() : <span className="empty">(Transaction pending)</span> }</td>
<td>{ item.date ? item.date.toLocaleTimeString() : <span className="empty">(Transaction pending)</span> }</td>
<td>
<a className="button-text" href={"https://explorer.lbry.io/tx/"+item.id} target="_blank">{item.id.substr(0, 7)}</a>
</td>
</tr>
);
});
}
return (
<section className="card">
<div className="card__title-primary">
<h3>Transaction History</h3>
</div>
<div className="card__content">
{ fetchingTransactions ? <BusyMessage message="Loading transactions" /> : '' }
{ !fetchingTransactions && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
{ rows.length > 0 ?
<table className="table-standard table-stretch">
<thead>
<tr>
<th>Amount</th>
<th>Date</th>
<th>Time</th>
<th>Transaction</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
: ''
}
</div>
</section>
)
}
const WalletPage = (props) => {
const {
balance,
currentPage
} = props
return (
<main className="main--single-column">
<section className="card">
<div className="card__title-primary">
<h3>Balance</h3>
</div>
<div className="card__content">
<CreditAmount amount={balance} precision={8} />
</div>
</section>
{ currentPage === 'wallet' ? <TransactionList {...props} /> : '' }
{ currentPage === 'send' ? <SendToAddressSection {...props} /> : '' }
{ currentPage === 'receive' ? <AddressSection {...props} /> : '' }
</main>
)
}
export default WalletPage;

View file

@ -6,13 +6,8 @@ const defaultState = {
currentPage: 'discover',
platform: process.platform,
drawerOpen: sessionStorage.getItem('drawerOpen') || true,
upgradeSkipped: sessionStorage.getItem('upgradeSkipped')
}
reducers[types.UPDATE_BALANCE] = function(state, action) {
return Object.assign({}, state, {
balance: action.data.balance
})
upgradeSkipped: sessionStorage.getItem('upgradeSkipped'),
daemonReady: false,
}
reducers[types.NAVIGATE] = function(state, action) {
@ -98,6 +93,13 @@ reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) {
})
}
reducers[types.DAEMON_READY] = function(state, action) {
// sessionStorage.setItem('loaded', 'y');
return Object.assign({}, state, {
daemonReady: true
})
}
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

52
ui/js/reducers/wallet.js Normal file
View file

@ -0,0 +1,52 @@
import * as types from 'constants/action_types'
const reducers = {}
const address = sessionStorage.getItem('receiveAddress')
const defaultState = {
balance: 0,
transactions: [],
fetchingTransactions: false,
receiveAddress: address,
gettingNewAddress: false,
}
reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
return Object.assign({}, state, {
fetchingTransactions: true
})
}
reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
return Object.assign({}, state, {
transactions: action.data.transactions,
fetchingTransactions: false
})
}
reducers[types.GET_NEW_ADDRESS_STARTED] = function(state, action) {
return Object.assign({}, state, {
gettingNewAddress: true
})
}
reducers[types.GET_NEW_ADDRESS_COMPLETED] = function(state, action) {
const { address } = action.data
sessionStorage.setItem('receiveAddress', address)
return Object.assign({}, state, {
gettingNewAddress: false,
receiveAddress: address
})
}
reducers[types.UPDATE_BALANCE] = function(state, action) {
return Object.assign({}, state, {
balance: action.data.balance
})
}
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -16,13 +16,6 @@ export const selectCurrentPage = createSelector(
}
)
export const selectBalance = createSelector(
_selectState,
(state) => {
return state.balance || 0
}
)
export const selectPlatform = createSelector(
_selectState,
(state) => {
@ -101,17 +94,17 @@ export const selectHeaderLinks = createSelector(
case 'claim':
case 'referral':
return {
'?wallet' : 'Overview',
'?send' : 'Send',
'?receive' : 'Receive',
'?claim' : 'Claim Beta Code',
'?referral' : 'Check Referral Credit',
'wallet' : 'Overview',
'send' : 'Send',
'receive' : 'Receive',
'claim' : 'Claim Beta Code',
'referral' : 'Check Referral Credit',
};
case 'downloaded':
case 'published':
return {
'?downloaded': 'Downloaded',
'?published': 'Published',
'downloaded': 'Downloaded',
'published': 'Published',
};
default:
return null;
@ -143,3 +136,8 @@ export const selectError = createSelector(
_selectState,
(state) => state.error
)
export const selectDaemonReady = createSelector(
_selectState,
(state) => state.daemonReady
)

109
ui/js/selectors/wallet.js Normal file
View file

@ -0,0 +1,109 @@
import { createSelector } from 'reselect'
import {
selectCurrentPage,
} from 'selectors/app'
export const _selectState = state => state.wallet || {}
export const selectBalance = createSelector(
_selectState,
(state) => {
return state.balance || 0
}
)
export const selectTransactions = createSelector(
_selectState,
(state) => state.transactions
)
export const selectTransactionItems = createSelector(
selectTransactions,
(transactions) => {
if (transactions.length == 0) return transactions
const transactionItems = []
const condensedTransactions = {}
transactions.forEach(function(tx) {
const txid = tx["txid"];
if (!(txid in condensedTransactions)) {
condensedTransactions[txid] = 0;
}
condensedTransactions[txid] += parseFloat(tx["value"]);
});
transactions.reverse().forEach(function(tx) {
const txid = tx["txid"];
if (condensedTransactions[txid] && condensedTransactions[txid] != 0)
{
transactionItems.push({
id: txid,
date: tx["timestamp"] ? (new Date(parseInt(tx["timestamp"]) * 1000)) : null,
amount: condensedTransactions[txid]
});
delete condensedTransactions[txid];
}
});
return transactionItems
}
)
export const selectIsFetchingTransactions = createSelector(
_selectState,
(state) => state.fetchingTransactions
)
export const shouldFetchTransactions = createSelector(
selectCurrentPage,
selectTransactions,
selectIsFetchingTransactions,
(page, transactions, fetching) => {
if (page != 'wallet') return false
if (fetching) return false
if (transactions.length != 0) return false
return true
}
)
export const selectReceiveAddress = createSelector(
_selectState,
(state) => state.receiveAddress
)
export const selectGettingNewAddress = createSelector(
_selectState,
(state) => state.gettingNewAddress
)
export const selectDaemonReady = createSelector(
() => sessionStorage.getItem('loaded') == 'y'
)
export const shouldGetReceiveAddress = createSelector(
selectReceiveAddress,
selectGettingNewAddress,
selectDaemonReady,
(address, fetching, daemonReady) => {
if (!daemonReady) return false
if (fetching) return false
if (address !== undefined) return false
return true
}
)
export const shouldCheckAddressIsMine = createSelector(
_selectState,
selectCurrentPage,
selectReceiveAddress,
selectDaemonReady,
(state, page, address, daemonReady) => {
if (!daemonReady) return false
if (address === undefined) return false
if (state.addressOwnershipChecked) return false
return true
}
)

View file

@ -6,6 +6,7 @@ import {
createLogger
} from 'redux-logger'
import appReducer from 'reducers/app';
import walletReducer from 'reducers/wallet'
function isFunction(object) {
return typeof object === 'function';
@ -17,6 +18,7 @@ function isNotFunction(object) {
const reducers = redux.combineReducers({
app: appReducer,
wallet: walletReducer,
});
var middleware = [thunk]

33
ui/js/triggers.js Normal file
View file

@ -0,0 +1,33 @@
import {
shouldFetchTransactions,
shouldGetReceiveAddress,
} from 'selectors/wallet'
import {
doFetchTransactions,
doGetNewAddress,
} from 'actions/wallet'
const triggers = []
triggers.push({
selector: shouldFetchTransactions,
action: doFetchTransactions,
})
triggers.push({
selector: shouldGetReceiveAddress,
action: doGetNewAddress
})
const runTriggers = function() {
triggers.forEach(function(trigger) {
const state = app.store.getState();
const should = trigger.selector(state)
if (trigger.selector(state)) app.store.dispatch(trigger.action())
});
}
module.exports = {
triggers: triggers,
runTriggers: runTriggers
}