From 089572880fff99e0832b427820ab5f682309ffc6 Mon Sep 17 00:00:00 2001 From: 6ea86b96 <6ea86b96@gmail.com> Date: Sat, 22 Apr 2017 20:17:01 +0700 Subject: [PATCH] Wallet progress --- ui/js/actions/app.js | 15 +- ui/js/actions/wallet.js | 60 ++++ ui/js/component/app/view.jsx | 3 +- ui/js/component/drawer/index.jsx | 8 +- ui/js/component/header/index.js | 23 ++ .../component/{header.js => header/view.jsx} | 22 +- ui/js/component/router/view.jsx | 4 +- ui/js/component/sub-header.js | 22 ++ ui/js/component/wallet-nav.js | 12 + ui/js/constants/action_types.js | 25 +- ui/js/main.js | 5 +- ui/js/page/help/view.jsx | 2 +- ui/js/page/wallet.js | 326 ------------------ ui/js/page/wallet/index.js | 39 +++ ui/js/page/wallet/view.jsx | 219 ++++++++++++ ui/js/reducers/app.js | 16 +- ui/js/reducers/wallet.js | 52 +++ ui/js/selectors/app.js | 26 +- ui/js/selectors/wallet.js | 109 ++++++ ui/js/store.js | 2 + ui/js/triggers.js | 33 ++ 21 files changed, 629 insertions(+), 394 deletions(-) create mode 100644 ui/js/actions/wallet.js create mode 100644 ui/js/component/header/index.js rename ui/js/component/{header.js => header/view.jsx} (90%) create mode 100644 ui/js/component/sub-header.js create mode 100644 ui/js/component/wallet-nav.js delete mode 100644 ui/js/page/wallet.js create mode 100644 ui/js/page/wallet/index.js create mode 100644 ui/js/page/wallet/view.jsx create mode 100644 ui/js/reducers/wallet.js create mode 100644 ui/js/selectors/wallet.js create mode 100644 ui/js/triggers.js diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 14aff78f7..073ae9099 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -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 + } +} diff --git a/ui/js/actions/wallet.js b/ui/js/actions/wallet.js new file mode 100644 index 000000000..3486b9be5 --- /dev/null +++ b/ui/js/actions/wallet.js @@ -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 + }) + }) + } +} diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index eef017835..c160e8146 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -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' diff --git a/ui/js/component/drawer/index.jsx b/ui/js/component/drawer/index.jsx index b5f666c92..3c4c524b0 100644 --- a/ui/js/component/drawer/index.jsx +++ b/ui/js/component/drawer/index.jsx @@ -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), diff --git a/ui/js/component/header/index.js b/ui/js/component/header/index.js new file mode 100644 index 000000000..3b9cb57ef --- /dev/null +++ b/ui/js/component/header/index.js @@ -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) diff --git a/ui/js/component/header.js b/ui/js/component/header/view.jsx similarity index 90% rename from ui/js/component/header.js rename to ui/js/component/header/view.jsx index f0d66caee..34627e14a 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header/view.jsx @@ -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( - - {this.props.links[link]} - - ); - } - return ( - - ); - } -}); - export default Header; diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index c6aad0588..208bb3f6d 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -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'; diff --git a/ui/js/component/sub-header.js b/ui/js/component/sub-header.js new file mode 100644 index 000000000..061b0e942 --- /dev/null +++ b/ui/js/component/sub-header.js @@ -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( + navigate(link)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }> + {subLinks[link]} + + ); + } + + return ( + + ) +} \ No newline at end of file diff --git a/ui/js/component/wallet-nav.js b/ui/js/component/wallet-nav.js new file mode 100644 index 000000000..51cd9278b --- /dev/null +++ b/ui/js/component/wallet-nav.js @@ -0,0 +1,12 @@ +import {SubHeader} from '../component/sub-header.js'; + +export let WalletNav = React.createClass({ + render: function () { + return ; + } +}); \ No newline at end of file diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 17f05441a..cafea5c7d 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -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' diff --git a/ui/js/main.js b/ui/js/main.js index 14c22c9c0..1f1d4c92f 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -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(
{ lbryio.enabled ? : '' }
, canvas) + ReactDOM.render(
{ lbryio.enabled ? : '' }
, canvas) } if (window.sessionStorage.getItem('loaded') == 'y') { diff --git a/ui/js/page/help/view.jsx b/ui/js/page/help/view.jsx index eba7a6273..b5fe61787 100644 --- a/ui/js/page/help/view.jsx +++ b/ui/js/page/help/view.jsx @@ -48,7 +48,7 @@ var HelpPage = React.createClass({ } return ( -
+

Read the FAQ

Our FAQ answers many common questions.

diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js deleted file mode 100644 index a9b3a92a0..000000000 --- a/ui/js/page/wallet.js +++ /dev/null @@ -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 ( -
-
-

Wallet Address

-
-
-
-
-
- -
-
-
-

Other LBRY users may send credits to you by entering this address on the "Send" page.

-

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.

-
-
-
- ); - } -}); - -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: , - 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 ( -
-
-
-

Send Credits

-
-
- -
-
- -
-
- 0.0) || this.state.address == ""} /> - -
- { - this.state.results ? -
-

Results

- {this.state.results} -
: '' - } -
- - Insufficient balance: after this transaction you would have less than 1 LBC in your wallet. - -
- ); - } -}); - - -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( - - { (item.amount > 0 ? '+' : '' ) + item.amount } - { item.date ? item.date.toLocaleDateString() : (Transaction pending) } - { item.date ? item.date.toLocaleTimeString() : (Transaction pending) } - - {item.id.substr(0, 7)} - - - ); - }); - } - return ( -
-
-

Transaction History

-
-
- { this.state.transactionItems === null ? : '' } - { this.state.transactionItems && rows.length === 0 ?
You have no transactions.
: '' } - { this.state.transactionItems && rows.length > 0 ? - - - - - - - - - - - {rows} - -
AmountDateTimeTransaction
- : '' - } -
-
- ); - } -}); - -export let WalletNav = React.createClass({ - render: function() { - return ; - } -}); - -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 ( -
- -
-
-

Balance

-
-
- { this.state.balance === null ? : ''} - { this.state.balance !== null ? : '' } -
-
- { this.props.viewingPage === 'wallet' ? : '' } - { this.props.viewingPage === 'send' ? : '' } - { this.props.viewingPage === 'receive' ? : '' } -
- ); - } -}); - -export default WalletPage; diff --git a/ui/js/page/wallet/index.js b/ui/js/page/wallet/index.js new file mode 100644 index 000000000..29a56012b --- /dev/null +++ b/ui/js/page/wallet/index.js @@ -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) diff --git a/ui/js/page/wallet/view.jsx b/ui/js/page/wallet/view.jsx new file mode 100644 index 000000000..7a7f9de0b --- /dev/null +++ b/ui/js/page/wallet/view.jsx @@ -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 ( +
+
+

Wallet Address

+
+
+
+
+
+ +
+
+
+

Other LBRY users may send credits to you by entering this address on the "Send" page.

+

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.

+
+
+
+ ); +} + +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: , + 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 ( +
+
+
+

Send Credits

+
+
+ +
+
+ +
+
+ 0.0) || this.state.address == ""} /> + +
+ { + this.state.results ? +
+

Results

+ {this.state.results} +
: '' + } +
+ + Insufficient balance: after this transaction you would have less than 1 LBC in your wallet. + +
+ ); + } +}); + +const TransactionList = (props) => { + const { + transactions, + fetchingTransactions, + transactionItems, + } = props + + const rows = [] + if (transactions.length > 0) { + transactionItems.forEach(function(item) { + rows.push( + + { (item.amount > 0 ? '+' : '' ) + item.amount } + { item.date ? item.date.toLocaleDateString() : (Transaction pending) } + { item.date ? item.date.toLocaleTimeString() : (Transaction pending) } + + {item.id.substr(0, 7)} + + + ); + }); + } + + return ( +
+
+

Transaction History

+
+
+ { fetchingTransactions ? : '' } + { !fetchingTransactions && rows.length === 0 ?
You have no transactions.
: '' } + { rows.length > 0 ? + + + + + + + + + + + {rows} + +
AmountDateTimeTransaction
+ : '' + } +
+
+ ) +} + +const WalletPage = (props) => { + const { + balance, + currentPage + } = props + + return ( +
+
+
+

Balance

+
+
+ +
+
+ { currentPage === 'wallet' ? : '' } + { currentPage === 'send' ? : '' } + { currentPage === 'receive' ? : '' } +
+ ) +} + +export default WalletPage; diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js index 6859b60f3..2d49207be 100644 --- a/ui/js/reducers/app.js +++ b/ui/js/reducers/app.js @@ -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); diff --git a/ui/js/reducers/wallet.js b/ui/js/reducers/wallet.js new file mode 100644 index 000000000..7a719f417 --- /dev/null +++ b/ui/js/reducers/wallet.js @@ -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; +} diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 0bb22541b..fffd8b581 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -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 +) diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js new file mode 100644 index 000000000..5e9576ff9 --- /dev/null +++ b/ui/js/selectors/wallet.js @@ -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 + } +) diff --git a/ui/js/store.js b/ui/js/store.js index 9d6ac13c4..3d40d0d92 100644 --- a/ui/js/store.js +++ b/ui/js/store.js @@ -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] diff --git a/ui/js/triggers.js b/ui/js/triggers.js new file mode 100644 index 000000000..85d44f360 --- /dev/null +++ b/ui/js/triggers.js @@ -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 +}