Wallet progress
This commit is contained in:
parent
cee2f2a626
commit
089572880f
21 changed files with 629 additions and 394 deletions
|
@ -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
60
ui/js/actions/wallet.js
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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),
|
||||
|
|
23
ui/js/component/header/index.js
Normal file
23
ui/js/component/header/index.js
Normal 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)
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
22
ui/js/component/sub-header.js
Normal file
22
ui/js/component/sub-header.js
Normal 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>
|
||||
)
|
||||
}
|
12
ui/js/component/wallet-nav.js
Normal file
12
ui/js/component/wallet-nav.js
Normal 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'
|
||||
}} />;
|
||||
}
|
||||
});
|
|
@ -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'
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
39
ui/js/page/wallet/index.js
Normal file
39
ui/js/page/wallet/index.js
Normal 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
219
ui/js/page/wallet/view.jsx
Normal 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;
|
|
@ -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
52
ui/js/reducers/wallet.js
Normal 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;
|
||||
}
|
|
@ -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
109
ui/js/selectors/wallet.js
Normal 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
|
||||
}
|
||||
)
|
|
@ -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
33
ui/js/triggers.js
Normal 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
|
||||
}
|
Loading…
Reference in a new issue