34 changed files with 1714 additions and 429 deletions
@ -0,0 +1,18 @@ |
|||
import React from 'react' |
|||
import { Flex } from 'rebass' |
|||
import { Button, Text } from 'components/UI' |
|||
import PlusCircle from 'components/Icon/PlusCircle' |
|||
|
|||
const CreateWalletButton = ({ ...rest }) => ( |
|||
<Button {...rest} variant="secondary"> |
|||
<Flex alignItem="center"> |
|||
<Text color="lightningOrange"> |
|||
<PlusCircle width="22px" height="22px" /> |
|||
</Text> |
|||
<Text lineHeight="22px" ml={2}> |
|||
Create new wallet |
|||
</Text> |
|||
</Flex> |
|||
</Button> |
|||
) |
|||
export default CreateWalletButton |
@ -0,0 +1,126 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Route, Switch, withRouter } from 'react-router-dom' |
|||
import { Box } from 'rebass' |
|||
import { Bar, Heading, MainContent, Sidebar } from 'components/UI' |
|||
import ZapLogo from 'components/Icon/ZapLogo' |
|||
import { CreateWalletButton, WalletLauncher, WalletsMenu, WalletUnlocker } from '.' |
|||
|
|||
const NoMatch = () => ( |
|||
<Box> |
|||
<Heading>Please select a wallet</Heading> |
|||
</Box> |
|||
) |
|||
|
|||
class Home extends React.Component { |
|||
static propTypes = { |
|||
activeWallet: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|||
deleteWallet: PropTypes.func.isRequired, |
|||
history: PropTypes.object.isRequired, |
|||
lightningGrpcActive: PropTypes.bool.isRequired, |
|||
walletUnlockerGrpcActive: PropTypes.bool.isRequired, |
|||
wallets: PropTypes.array.isRequired, |
|||
startLnd: PropTypes.func.isRequired, |
|||
stopLnd: PropTypes.func.isRequired, |
|||
setActiveWallet: PropTypes.func.isRequired, |
|||
unlockWallet: PropTypes.func.isRequired, |
|||
setUnlockWalletError: PropTypes.func.isRequired, |
|||
unlockingWallet: PropTypes.bool, |
|||
unlockWalletError: PropTypes.string |
|||
} |
|||
|
|||
/** |
|||
* Handle click event on the Create new wallet button, |
|||
*/ |
|||
handleCreateNewWalletClick = () => { |
|||
const { history } = this.props |
|||
history.push('/onboarding') |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
activeWallet, |
|||
deleteWallet, |
|||
startLnd, |
|||
unlockWallet, |
|||
wallets, |
|||
setActiveWallet, |
|||
stopLnd, |
|||
lightningGrpcActive, |
|||
walletUnlockerGrpcActive, |
|||
setUnlockWalletError, |
|||
unlockingWallet, |
|||
unlockWalletError |
|||
} = this.props |
|||
|
|||
return ( |
|||
<> |
|||
<Sidebar.small p={3} pt={40}> |
|||
<ZapLogo width="70px" height="32px" /> |
|||
|
|||
<WalletsMenu |
|||
wallets={wallets} |
|||
mt={30} |
|||
activeWallet={activeWallet} |
|||
setActiveWallet={setActiveWallet} |
|||
/> |
|||
|
|||
<Box width={1} css={{ position: 'absolute', left: 0, bottom: 0 }} px={3}> |
|||
<Bar mx={-3} /> |
|||
<CreateWalletButton onClick={this.handleCreateNewWalletClick} width={1} p={3} /> |
|||
</Box> |
|||
</Sidebar.small> |
|||
|
|||
<MainContent width={1 / 2}> |
|||
<Box px={3} mt={72}> |
|||
<Switch> |
|||
<Route |
|||
exact |
|||
path="/home/wallet/:walletId" |
|||
render={({ match: { params } }) => { |
|||
const wallet = wallets.find(wallet => wallet.id == params.walletId) |
|||
if (!wallet) { |
|||
return null |
|||
} |
|||
return ( |
|||
<WalletLauncher |
|||
wallet={wallet} |
|||
startLnd={startLnd} |
|||
stopLnd={stopLnd} |
|||
lightningGrpcActive={lightningGrpcActive} |
|||
walletUnlockerGrpcActive={walletUnlockerGrpcActive} |
|||
deleteWallet={deleteWallet} |
|||
/> |
|||
) |
|||
}} |
|||
/> |
|||
<Route |
|||
exact |
|||
path="/home/wallet/:walletId/unlock" |
|||
render={({ match: { params } }) => { |
|||
const wallet = wallets.find(wallet => wallet.id == params.walletId) |
|||
if (!wallet) { |
|||
return null |
|||
} |
|||
return ( |
|||
<WalletUnlocker |
|||
wallet={wallet} |
|||
unlockWallet={unlockWallet} |
|||
lightningGrpcActive={lightningGrpcActive} |
|||
setUnlockWalletError={setUnlockWalletError} |
|||
unlockingWallet={unlockingWallet} |
|||
unlockWalletError={unlockWalletError} |
|||
/> |
|||
) |
|||
}} |
|||
/> |
|||
<Route component={NoMatch} /> |
|||
</Switch> |
|||
</Box> |
|||
</MainContent> |
|||
</> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withRouter(Home) |
@ -0,0 +1,18 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Box } from 'rebass' |
|||
import { Heading, Truncate } from 'components/UI' |
|||
|
|||
const WalletHeader = ({ title }) => ( |
|||
<Box> |
|||
<Heading.h1 fontSize="xxl"> |
|||
<Truncate text={title} maxlen={25} /> |
|||
</Heading.h1> |
|||
</Box> |
|||
) |
|||
|
|||
WalletHeader.propTypes = { |
|||
title: PropTypes.string.isRequired |
|||
} |
|||
|
|||
export default WalletHeader |
@ -0,0 +1,117 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { withRouter } from 'react-router-dom' |
|||
import { Box, Flex } from 'rebass' |
|||
import { Bar, Button, Heading } from 'components/UI' |
|||
import ArrowRight from 'components/Icon/ArrowRight' |
|||
import { WalletSettingsFormLocal, WalletSettingsFormRemote, WalletHeader } from '.' |
|||
|
|||
class WalletLauncher extends React.Component { |
|||
static propTypes = { |
|||
wallet: PropTypes.object.isRequired, |
|||
deleteWallet: PropTypes.func.isRequired, |
|||
startLnd: PropTypes.func.isRequired, |
|||
lightningGrpcActive: PropTypes.bool.isRequired, |
|||
walletUnlockerGrpcActive: PropTypes.bool.isRequired, |
|||
stopLnd: PropTypes.func.isRequired, |
|||
history: PropTypes.shape({ |
|||
push: PropTypes.func.isRequired |
|||
}) |
|||
} |
|||
|
|||
componentDidMount() { |
|||
const { stopLnd } = this.props |
|||
stopLnd() |
|||
} |
|||
|
|||
/** |
|||
* Redirect to the login page when we establish a connection to lnd. |
|||
*/ |
|||
componentDidUpdate(prevProps) { |
|||
const { history, lightningGrpcActive, walletUnlockerGrpcActive, wallet } = this.props |
|||
|
|||
// If the wallet unlocker became active, switch to the login screen
|
|||
if (walletUnlockerGrpcActive && !prevProps.walletUnlockerGrpcActive) { |
|||
history.push(`/home/wallet/${wallet.id}/unlock`) |
|||
} |
|||
|
|||
// If an active wallet connection has been established, switch to the app.
|
|||
if (lightningGrpcActive && !prevProps.lightningGrpcActive) { |
|||
if (wallet.type === 'local') { |
|||
history.push('/syncing') |
|||
} else { |
|||
history.push('/app') |
|||
} |
|||
} |
|||
} |
|||
|
|||
walletName = wallet => { |
|||
if (wallet.type === 'local') { |
|||
return wallet.alias || `Wallet #${wallet.id}` |
|||
} |
|||
return wallet.host.split(':')[0] |
|||
} |
|||
|
|||
handleDelete = () => { |
|||
const { deleteWallet, wallet } = this.props |
|||
deleteWallet(wallet.id) |
|||
} |
|||
|
|||
render() { |
|||
const { startLnd, wallet } = this.props |
|||
const walletName = this.walletName(wallet) |
|||
|
|||
return ( |
|||
<React.Fragment> |
|||
<Flex mb={4} alignItems="center"> |
|||
<WalletHeader title={walletName} /> |
|||
|
|||
<Box ml="auto"> |
|||
<Button type="button" size="small" onClick={this.handleDelete} mr={2}> |
|||
delete |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
size="small" |
|||
variant="primary" |
|||
form={`wallet-settings-form-${wallet.id}`} |
|||
> |
|||
<Flex> |
|||
<Box mr={1}>Launch now</Box> |
|||
<Box> |
|||
<ArrowRight /> |
|||
</Box> |
|||
</Flex> |
|||
</Button> |
|||
</Box> |
|||
</Flex> |
|||
|
|||
{wallet.type === 'local' && ( |
|||
<> |
|||
<Heading.h2 mb={4}>Settings</Heading.h2> |
|||
<Bar my={2} /> |
|||
<WalletSettingsFormLocal |
|||
key={wallet.id} |
|||
id={`wallet-settings-form-${wallet.id}`} |
|||
wallet={wallet} |
|||
startLnd={startLnd} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{wallet.type !== 'local' && ( |
|||
<> |
|||
<WalletSettingsFormRemote |
|||
key={wallet.id} |
|||
id={`wallet-settings-form-${wallet.id}`} |
|||
wallet={wallet} |
|||
startLnd={startLnd} |
|||
/> |
|||
</> |
|||
)} |
|||
</React.Fragment> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withRouter(WalletLauncher) |
@ -0,0 +1,221 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Flex } from 'rebass' |
|||
import { DataRow, Form, Input, Label, Range, Toggle } from 'components/UI' |
|||
import * as yup from 'yup' |
|||
|
|||
class WalletSettingsFormLocal extends React.Component { |
|||
static propTypes = { |
|||
wallet: PropTypes.object.isRequired, |
|||
startLnd: PropTypes.func.isRequired |
|||
} |
|||
|
|||
validateAutopilot = value => { |
|||
try { |
|||
yup.boolean().validateSync(value) |
|||
} catch (error) { |
|||
return error.message |
|||
} |
|||
} |
|||
|
|||
validateAutopilotAllocation = value => { |
|||
try { |
|||
yup |
|||
.number() |
|||
.required() |
|||
.positive() |
|||
.min(0) |
|||
.max(100) |
|||
.typeError('A number is required') |
|||
.validateSync(value) |
|||
} catch (error) { |
|||
return error.message |
|||
} |
|||
} |
|||
|
|||
validateAutopilotMaxchannels = value => { |
|||
try { |
|||
yup |
|||
.number() |
|||
.required() |
|||
.positive() |
|||
.integer() |
|||
.max(100) |
|||
.typeError('A number is required') |
|||
.validateSync(value) |
|||
} catch (error) { |
|||
return error.message |
|||
} |
|||
} |
|||
|
|||
validateAutopilotChansize = value => { |
|||
try { |
|||
yup |
|||
.number() |
|||
.required() |
|||
.positive() |
|||
.integer() |
|||
.max(100000000) |
|||
.typeError('A number is required') |
|||
.validateSync(value) |
|||
} catch (error) { |
|||
return error.message |
|||
} |
|||
} |
|||
|
|||
preSubmit = values => { |
|||
if (values.autopilotAllocation) { |
|||
values.autopilotAllocation = values.autopilotAllocation / 100 |
|||
} |
|||
return values |
|||
} |
|||
|
|||
onSubmit = async values => { |
|||
const { startLnd } = this.props |
|||
return startLnd(values) |
|||
} |
|||
|
|||
setFormApi = formApi => { |
|||
this.formApi = formApi |
|||
} |
|||
|
|||
render() { |
|||
const { wallet, startLnd, ...rest } = this.props |
|||
|
|||
return ( |
|||
<Form |
|||
getApi={this.setFormApi} |
|||
preSubmit={this.preSubmit} |
|||
onSubmit={this.onSubmit} |
|||
{...rest} |
|||
initialValues={wallet} |
|||
wallet={wallet} |
|||
> |
|||
{({ formState }) => ( |
|||
<React.Fragment> |
|||
<DataRow |
|||
py={2} |
|||
left={<Label htmlFor="alias">Alias</Label>} |
|||
right={<Input field="alias" id="alias" initialValue={wallet.alias} width={1} />} |
|||
/> |
|||
|
|||
<DataRow |
|||
py={2} |
|||
left={<Label htmlFor="autopilot">Autopilot</Label>} |
|||
right={ |
|||
<Toggle |
|||
field="autopilot" |
|||
id="autopilot" |
|||
validate={this.validateAutopilot} |
|||
validateOnBlur |
|||
validateOnChange={formState.invalid} |
|||
initialValue={wallet.autopilot} |
|||
/> |
|||
} |
|||
/> |
|||
|
|||
{formState.values.autopilot ? ( |
|||
<React.Fragment> |
|||
<DataRow |
|||
py={2} |
|||
left={<Label htmlFor="autopilotAllocation">Percentage of Balance</Label>} |
|||
right={ |
|||
<Flex alignItems="center" ml="auto"> |
|||
<Range |
|||
field="autopilotAllocation" |
|||
id="autopilotAllocation" |
|||
initialValue={wallet.autopilotAllocation * 100} |
|||
validate={this.validateAutopilotAllocation} |
|||
validateOnChange={formState.invalid} |
|||
validateOnBlur |
|||
ml="auto" |
|||
min="0" |
|||
max="100" |
|||
step="1" |
|||
width={1} |
|||
/> |
|||
<Input |
|||
field="autopilotAllocation" |
|||
id="autopilotAllocation" |
|||
type="number" |
|||
variant="thin" |
|||
ml={2} |
|||
width={100} |
|||
/> |
|||
</Flex> |
|||
} |
|||
/> |
|||
|
|||
<DataRow |
|||
py={2} |
|||
left={<Label htmlFor="autopilotMaxchannels">Number of Channels max</Label>} |
|||
right={ |
|||
<Input |
|||
field="autopilotMaxchannels" |
|||
id="autopilotMaxchannels" |
|||
variant="thin" |
|||
type="number" |
|||
initialValue={wallet.autopilotMaxchannels} |
|||
validate={this.validateAutopilotMaxchannels} |
|||
validateOnChange={formState.invalid} |
|||
validateOnBlur |
|||
step="1" |
|||
width={100} |
|||
ml="auto" |
|||
/> |
|||
} |
|||
/> |
|||
|
|||
<DataRow |
|||
py={2} |
|||
left={<Label htmlFor="autopilotMinchansize">Minimum channel size</Label>} |
|||
right={ |
|||
<Input |
|||
field="autopilotMinchansize" |
|||
id="autopilotMinchansize" |
|||
variant="thin" |
|||
type="number" |
|||
min="0" |
|||
max="100000000" |
|||
step="1" |
|||
initialValue={wallet.autopilotMinchansize} |
|||
validate={this.validateAutopilotChansize} |
|||
validateOnBlur |
|||
validateOnChange={formState.invalid} |
|||
width={100} |
|||
ml="auto" |
|||
/> |
|||
} |
|||
/> |
|||
|
|||
<DataRow |
|||
py={2} |
|||
left={<Label htmlFor="autopilotMaxchansize">Maximum channel size</Label>} |
|||
right={ |
|||
<Input |
|||
field="autopilotMaxchansize" |
|||
id="autopilotMaxchansize" |
|||
variant="thin" |
|||
type="number" |
|||
min="0" |
|||
max="100000000" |
|||
step="1" |
|||
initialValue={wallet.autopilotMaxchansize} |
|||
validate={this.validateAutopilotChansize} |
|||
validateOnChange={formState.invalid} |
|||
validateOnBlur |
|||
width={100} |
|||
ml="auto" |
|||
/> |
|||
} |
|||
/> |
|||
</React.Fragment> |
|||
) : null} |
|||
</React.Fragment> |
|||
)} |
|||
</Form> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default WalletSettingsFormLocal |
@ -0,0 +1,39 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Card } from 'rebass' |
|||
import { Form } from 'components/UI' |
|||
|
|||
class WalletSettingsFormRemote extends React.Component { |
|||
static propTypes = { |
|||
wallet: PropTypes.object.isRequired, |
|||
startLnd: PropTypes.func.isRequired |
|||
} |
|||
|
|||
onSubmit = async values => { |
|||
const { startLnd } = this.props |
|||
return startLnd(values) |
|||
} |
|||
|
|||
setFormApi = formApi => { |
|||
this.formApi = formApi |
|||
} |
|||
|
|||
render() { |
|||
const { wallet, startLnd, ...rest } = this.props |
|||
return ( |
|||
<Form |
|||
getApi={this.setFormApi} |
|||
onSubmit={this.onSubmit} |
|||
initialValues={wallet} |
|||
wallet={wallet} |
|||
{...rest} |
|||
> |
|||
<Card bg="tertiaryColor" my={3} p={3}> |
|||
<pre>{JSON.stringify(wallet, null, 2)}</pre> |
|||
</Card> |
|||
</Form> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default WalletSettingsFormRemote |
@ -0,0 +1,130 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { withRouter } from 'react-router-dom' |
|||
import { Form } from 'informed' |
|||
import { Button, PasswordInput } from 'components/UI' |
|||
import * as yup from 'yup' |
|||
import { WalletHeader } from '.' |
|||
|
|||
/** |
|||
* @render react |
|||
* @name WalletUnlocker |
|||
* @example |
|||
* <WalletUnlocker |
|||
wallet={{ ... }} |
|||
unlockWallet={() => {}} |
|||
setError={() => {}} > |
|||
*/ |
|||
class WalletUnlocker extends React.Component { |
|||
static displayName = 'WalletUnlocker' |
|||
|
|||
static propTypes = { |
|||
wallet: PropTypes.object.isRequired, |
|||
lightningGrpcActive: PropTypes.bool.isRequired, |
|||
history: PropTypes.shape({ |
|||
push: PropTypes.func.isRequired |
|||
}), |
|||
unlockingWallet: PropTypes.bool, |
|||
unlockWalletError: PropTypes.string, |
|||
unlockWallet: PropTypes.func.isRequired, |
|||
setUnlockWalletError: PropTypes.func.isRequired |
|||
} |
|||
|
|||
componentDidUpdate(prevProps) { |
|||
const { |
|||
wallet, |
|||
lightningGrpcActive, |
|||
history, |
|||
setUnlockWalletError, |
|||
unlockingWallet, |
|||
unlockWalletError |
|||
} = this.props |
|||
|
|||
// Set the form error if we got an error unlocking.
|
|||
if (unlockWalletError && !prevProps.unlockWalletError) { |
|||
this.formApi.setError('password', unlockWalletError) |
|||
setUnlockWalletError(null) |
|||
} |
|||
|
|||
// Redirect to the app if the wallet was successfully unlocked.
|
|||
if (!unlockingWallet && prevProps.unlockingWallet && !unlockWalletError) { |
|||
if (wallet.type === 'local') { |
|||
history.push('/syncing') |
|||
} else { |
|||
history.push('/app') |
|||
} |
|||
} |
|||
|
|||
// If an active wallet connection has been established, switch to the app.
|
|||
if (lightningGrpcActive && !prevProps.lightningGrpcActive) { |
|||
if (wallet.type === 'local') { |
|||
history.push('/syncing') |
|||
} else { |
|||
history.push('/app') |
|||
} |
|||
} |
|||
} |
|||
|
|||
setFormApi = formApi => { |
|||
this.formApi = formApi |
|||
} |
|||
|
|||
onSubmit = values => { |
|||
const { unlockWallet } = this.props |
|||
unlockWallet(values.password) |
|||
} |
|||
|
|||
walletName = wallet => { |
|||
if (wallet.type === 'local') { |
|||
return wallet.alias || `Wallet #${wallet.id}` |
|||
} |
|||
return wallet.host.split(':')[0] |
|||
} |
|||
|
|||
validatePassword = value => { |
|||
try { |
|||
yup |
|||
.string() |
|||
.required() |
|||
.min(8) |
|||
.validateSync(value) |
|||
} catch (error) { |
|||
return error.message |
|||
} |
|||
} |
|||
|
|||
render = () => { |
|||
const { wallet } = this.props |
|||
const walletName = this.walletName(wallet) |
|||
|
|||
return ( |
|||
<Form |
|||
width={1} |
|||
getApi={this.setFormApi} |
|||
onSubmit={this.onSubmit} |
|||
onSubmitFailure={this.onSubmitFailure} |
|||
key={`wallet-unlocker-form-${wallet.id}`} |
|||
> |
|||
{({ formState }) => ( |
|||
<React.Fragment> |
|||
<WalletHeader title={walletName} /> |
|||
|
|||
<PasswordInput |
|||
field="password" |
|||
id="password" |
|||
label="Enter Password" |
|||
my={3} |
|||
validate={this.validatePassword} |
|||
validateOnBlur |
|||
validateOnChange={formState.invalid} |
|||
/> |
|||
|
|||
<Button type="submit">Enter</Button> |
|||
</React.Fragment> |
|||
)} |
|||
</Form> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withRouter(WalletUnlocker) |
@ -0,0 +1,60 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { NavLink } from 'react-router-dom' |
|||
import { Box } from 'rebass' |
|||
import { Text } from 'components/UI' |
|||
|
|||
const walletName = wallet => { |
|||
if (wallet.type === 'local') { |
|||
return wallet.alias || `Wallet #${wallet.id}` |
|||
} |
|||
return wallet.host.split(':')[0] |
|||
} |
|||
|
|||
const WalletGroup = ({ setActiveWallet, title, wallets, ...rest }) => ( |
|||
<Box {...rest}> |
|||
<Text fontWeight="normal">{title}</Text> |
|||
{wallets.map(wallet => ( |
|||
<Text key={wallet.id} py={1}> |
|||
<NavLink |
|||
to={`/home/wallet/${wallet.id}`} |
|||
activeStyle={{ fontWeight: 'normal' }} |
|||
onClick={() => setActiveWallet(wallet.id)} |
|||
> |
|||
{walletName(wallet)} |
|||
</NavLink> |
|||
</Text> |
|||
))} |
|||
</Box> |
|||
) |
|||
|
|||
class WalletsMenu extends React.Component { |
|||
static displayName = 'WalletsMenu' |
|||
|
|||
static propTypes = { |
|||
setActiveWallet: PropTypes.func.isRequired, |
|||
wallets: PropTypes.array.isRequired |
|||
} |
|||
|
|||
render() { |
|||
const { setActiveWallet, wallets, ...rest } = this.props |
|||
const localWallets = wallets.filter(wallet => wallet.type === 'local') |
|||
const otherWallets = wallets.filter(wallet => wallet.type !== 'local') |
|||
|
|||
return ( |
|||
<Box {...rest}> |
|||
<WalletGroup |
|||
title="Your Wallets" |
|||
wallets={localWallets} |
|||
setActiveWallet={setActiveWallet} |
|||
mb={3} |
|||
/> |
|||
{otherWallets.length > 0 && ( |
|||
<WalletGroup title="More" wallets={otherWallets} setActiveWallet={setActiveWallet} /> |
|||
)} |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default WalletsMenu |
@ -0,0 +1,8 @@ |
|||
export Home from './Home' |
|||
export CreateWalletButton from './CreateWalletButton' |
|||
export WalletsMenu from './WalletsMenu' |
|||
export WalletLauncher from './WalletLauncher' |
|||
export WalletUnlocker from './WalletUnlocker' |
|||
export WalletSettingsFormLocal from './WalletSettingsFormLocal' |
|||
export WalletSettingsFormRemote from './WalletSettingsFormRemote' |
|||
export WalletHeader from './WalletHeader' |
@ -0,0 +1,28 @@ |
|||
import { connect } from 'react-redux' |
|||
import { setActiveWallet, walletSelectors, deleteWallet } from 'reducers/wallet' |
|||
import { setUnlockWalletError, stopLnd, startLnd, unlockWallet } from 'reducers/lnd' |
|||
import { Home } from 'components/Home' |
|||
|
|||
const mapStateToProps = state => ({ |
|||
wallets: state.wallet.wallets, |
|||
activeWallet: walletSelectors.activeWallet(state), |
|||
activeWalletSettings: walletSelectors.activeWalletSettings(state), |
|||
lightningGrpcActive: state.lnd.lightningGrpcActive, |
|||
walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive, |
|||
unlockingWallet: state.lnd.unlockingWallet, |
|||
unlockWalletError: state.lnd.unlockWalletError |
|||
}) |
|||
|
|||
const mapDispatchToProps = { |
|||
setActiveWallet, |
|||
setUnlockWalletError, |
|||
stopLnd, |
|||
startLnd, |
|||
unlockWallet, |
|||
deleteWallet |
|||
} |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(Home) |
@ -0,0 +1,92 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { connect } from 'react-redux' |
|||
import { withRouter } from 'react-router' |
|||
import { walletSelectors } from 'reducers/wallet' |
|||
import { startActiveWallet } from 'reducers/lnd' |
|||
|
|||
/** |
|||
* Root component that deals with mounting the app and managing top level routing. |
|||
*/ |
|||
class Initializer extends React.Component { |
|||
static propTypes = { |
|||
onboarding: PropTypes.bool, |
|||
history: PropTypes.object.isRequired, |
|||
activeWallet: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|||
activeWalletSettings: PropTypes.object, |
|||
isWalletOpen: PropTypes.bool, |
|||
lightningGrpcActive: PropTypes.bool, |
|||
walletUnlockerGrpcActive: PropTypes.bool, |
|||
startActiveWallet: PropTypes.func.isRequired |
|||
} |
|||
|
|||
/** |
|||
* Redirect to the correct page when we establish a connection to lnd. |
|||
*/ |
|||
componentDidUpdate(prevProps) { |
|||
const { |
|||
onboarding, |
|||
history, |
|||
activeWallet, |
|||
activeWalletSettings, |
|||
isWalletOpen, |
|||
lightningGrpcActive, |
|||
walletUnlockerGrpcActive, |
|||
startActiveWallet |
|||
} = this.props |
|||
|
|||
// Wait unti we are onboarding before doing anything.
|
|||
if (!onboarding) { |
|||
return |
|||
} |
|||
|
|||
// If we have just determined that the user has an active wallet, attempt to start it.
|
|||
if (typeof activeWallet !== 'undefined') { |
|||
if (activeWalletSettings) { |
|||
if (isWalletOpen) { |
|||
startActiveWallet() |
|||
} else { |
|||
history.push(`/home/wallet/${activeWallet}`) |
|||
} |
|||
} else { |
|||
history.push('/onboarding') |
|||
} |
|||
} |
|||
|
|||
// If the wallet unlocker became active, switch to the login screen
|
|||
if (walletUnlockerGrpcActive && !prevProps.walletUnlockerGrpcActive) { |
|||
history.push(`/home/wallet/${activeWallet}/unlock`) |
|||
} |
|||
|
|||
// If an active wallet connection has been established, switch to the app.
|
|||
if (lightningGrpcActive && !prevProps.lightningGrpcActive) { |
|||
if (activeWalletSettings.type === 'local') { |
|||
history.push('/syncing') |
|||
} else { |
|||
history.push('/app') |
|||
} |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
const mapStateToProps = state => ({ |
|||
onboarding: state.onboarding.onboarding, |
|||
activeWallet: walletSelectors.activeWallet(state), |
|||
activeWalletSettings: walletSelectors.activeWalletSettings(state), |
|||
lightningGrpcActive: state.lnd.lightningGrpcActive, |
|||
walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive, |
|||
isWalletOpen: state.wallet.isWalletOpen |
|||
}) |
|||
|
|||
const mapDispatchToProps = { |
|||
startActiveWallet |
|||
} |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(withRouter(Initializer)) |
@ -0,0 +1,44 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { connect } from 'react-redux' |
|||
import { withRouter } from 'react-router' |
|||
import { restart } from 'reducers/lnd' |
|||
import { setIsWalletOpen } from 'reducers/wallet' |
|||
|
|||
/** |
|||
* Root component that deals with mounting the app and managing top level routing. |
|||
*/ |
|||
class Logout extends React.Component { |
|||
static propTypes = { |
|||
lightningGrpcActive: PropTypes.bool, |
|||
walletUnlockerGrpcActive: PropTypes.bool, |
|||
restart: PropTypes.func.isRequired |
|||
} |
|||
|
|||
componentDidMount() { |
|||
const { lightningGrpcActive, walletUnlockerGrpcActive, restart } = this.props |
|||
if (lightningGrpcActive || walletUnlockerGrpcActive) { |
|||
setIsWalletOpen(false) |
|||
restart() |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
const mapStateToProps = state => ({ |
|||
lightningGrpcActive: state.lnd.lightningGrpcActive, |
|||
walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive |
|||
}) |
|||
|
|||
const mapDispatchToProps = { |
|||
restart, |
|||
setIsWalletOpen |
|||
} |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(withRouter(Logout)) |
@ -0,0 +1,132 @@ |
|||
import { createSelector } from 'reselect' |
|||
import db from 'store/db' |
|||
|
|||
// ------------------------------------
|
|||
// Constants
|
|||
// ------------------------------------
|
|||
export const SET_WALLETS = 'SET_WALLETS' |
|||
export const SET_ACTIVE_WALLET = 'SET_ACTIVE_WALLET' |
|||
export const SET_IS_WALLET_OPEN = 'SET_IS_WALLET_OPEN' |
|||
export const DELETE_WALLET = 'DELETE_WALLET' |
|||
export const PUT_WALLET = 'PUT_WALLET' |
|||
|
|||
// ------------------------------------
|
|||
// Actions
|
|||
// ------------------------------------
|
|||
|
|||
export function setWallets(wallets) { |
|||
return { |
|||
type: SET_WALLETS, |
|||
wallets |
|||
} |
|||
} |
|||
|
|||
export function setIsWalletOpen(isWalletOpen) { |
|||
db.settings.put({ |
|||
key: 'isWalletOpen', |
|||
value: isWalletOpen |
|||
}) |
|||
return { |
|||
type: SET_IS_WALLET_OPEN, |
|||
isWalletOpen |
|||
} |
|||
} |
|||
|
|||
export function setActiveWallet(activeWallet) { |
|||
db.settings.put({ |
|||
key: 'activeWallet', |
|||
value: activeWallet |
|||
}) |
|||
return { |
|||
type: SET_ACTIVE_WALLET, |
|||
activeWallet |
|||
} |
|||
} |
|||
|
|||
export const getWallets = () => async dispatch => { |
|||
let wallets |
|||
try { |
|||
wallets = await db.wallets.toArray() |
|||
} catch (e) { |
|||
wallets = [] |
|||
} |
|||
dispatch(setWallets(wallets)) |
|||
return wallets |
|||
} |
|||
|
|||
export const putWallet = wallet => async dispatch => { |
|||
dispatch({ type: PUT_WALLET, wallet }) |
|||
wallet.id = await db.wallets.put(wallet) |
|||
await dispatch(getWallets()) |
|||
return wallet |
|||
} |
|||
|
|||
export const deleteWallet = walletId => async dispatch => { |
|||
dispatch({ type: DELETE_WALLET, walletId }) |
|||
await db.wallets.delete(walletId) |
|||
const wallets = await dispatch(getWallets()) |
|||
dispatch(setActiveWallet(wallets[0].id)) |
|||
setIsWalletOpen(false) |
|||
} |
|||
|
|||
export const initWallets = () => async dispatch => { |
|||
let activeWallet |
|||
let isWalletOpen |
|||
try { |
|||
await dispatch(getWallets()) |
|||
activeWallet = await db.settings.get({ key: 'activeWallet' }) |
|||
activeWallet = activeWallet.value || null |
|||
|
|||
isWalletOpen = await db.settings.get({ key: 'isWalletOpen' }) |
|||
isWalletOpen = isWalletOpen.value |
|||
} catch (e) { |
|||
activeWallet = null |
|||
isWalletOpen = false |
|||
} |
|||
dispatch(setIsWalletOpen(isWalletOpen)) |
|||
dispatch(setActiveWallet(activeWallet)) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Action Handlers
|
|||
// ------------------------------------
|
|||
const ACTION_HANDLERS = { |
|||
[SET_WALLETS]: (state, { wallets }) => ({ ...state, wallets }), |
|||
[SET_ACTIVE_WALLET]: (state, { activeWallet }) => ({ ...state, activeWallet }), |
|||
[SET_IS_WALLET_OPEN]: (state, { isWalletOpen }) => ({ ...state, isWalletOpen }) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Selectors
|
|||
// ------------------------------------
|
|||
|
|||
const walletSelectors = {} |
|||
const activeWalletSelector = state => state.wallet.activeWallet |
|||
const walletsSelector = state => state.wallet.wallets |
|||
|
|||
walletSelectors.wallets = createSelector(walletsSelector, wallets => wallets) |
|||
walletSelectors.activeWallet = createSelector(activeWalletSelector, activeWallet => activeWallet) |
|||
walletSelectors.activeWalletSettings = createSelector( |
|||
walletsSelector, |
|||
activeWalletSelector, |
|||
(wallets, activeWallet) => wallets.find(wallet => wallet.id === activeWallet) |
|||
) |
|||
walletSelectors.hasWallets = createSelector(walletSelectors.wallets, wallets => wallets.length > 0) |
|||
|
|||
export { walletSelectors } |
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
|
|||
const initialState = { |
|||
isWalletOpen: false, |
|||
activeWallet: undefined, |
|||
wallets: [] |
|||
} |
|||
|
|||
export default function walletReducer(state = initialState, action) { |
|||
const handler = ACTION_HANDLERS[action.type] |
|||
|
|||
return handler ? handler(state, action) : state |
|||
} |
@ -0,0 +1,112 @@ |
|||
import React from 'react' |
|||
import { storiesOf } from '@storybook/react' |
|||
import { linkTo } from '@storybook/addon-links' |
|||
import { State, Store } from '@sambego/storybook-state' |
|||
import StoryRouter from 'storybook-react-router' |
|||
import { Page } from 'components/UI' |
|||
import { Home } from 'components/Home' |
|||
|
|||
const delay = time => new Promise(resolve => setTimeout(() => resolve(), time)) |
|||
|
|||
const store = new Store({ |
|||
activeWallet: 1, |
|||
lightningGrpcActive: false, |
|||
walletUnlockerGrpcActive: false, |
|||
unlockingWallet: false, |
|||
unlockWalletError: '', |
|||
wallets: [ |
|||
{ |
|||
id: 1, |
|||
autopilot: true, |
|||
autopilotAllocation: 0.6, |
|||
autopilotMaxchannels: 5, |
|||
autopilotMaxchansize: 16777215, |
|||
autopilotMinchansize: 20000, |
|||
autopilotMinconfs: 0, |
|||
autopilotPrivate: true, |
|||
chain: 'bitcoin', |
|||
network: 'testnet', |
|||
type: 'local' |
|||
}, |
|||
{ |
|||
id: 2, |
|||
autopilot: true, |
|||
autopilotAllocation: 0.9, |
|||
autopilotMaxchannels: 10, |
|||
autopilotMaxchansize: 16777215, |
|||
autopilotMinchansize: 20000, |
|||
autopilotMinconfs: 1, |
|||
autopilotPrivate: false, |
|||
chain: 'bitcoin', |
|||
network: 'mainnet', |
|||
type: 'local' |
|||
}, |
|||
{ |
|||
id: 3, |
|||
type: 'custom', |
|||
chain: 'bitcoin', |
|||
network: 'testnet', |
|||
host: 'mynode.local' |
|||
}, |
|||
{ |
|||
id: 4, |
|||
alias: 'The Lightning Store', |
|||
type: 'btcpayserver', |
|||
chain: 'bitcoin', |
|||
network: 'testnet', |
|||
host: 'example.btcpay.store' |
|||
} |
|||
] |
|||
}) |
|||
|
|||
const startLnd = async wallet => { |
|||
console.log('startLnd', wallet) |
|||
await delay(500) |
|||
store.set({ walletUnlockerGrpcActive: true, lightningGrpcActive: false }) |
|||
} |
|||
const stopLnd = async () => { |
|||
console.log('stopLnd') |
|||
await delay(500) |
|||
store.set({ walletUnlockerGrpcActive: false, lightningGrpcActive: false }) |
|||
} |
|||
const unlockWallet = async (wallet, password) => { |
|||
console.log('unlockWallet', wallet, password) |
|||
await delay(300) |
|||
store.set({ walletUnlockerGrpcActive: false, lightningGrpcActive: true }) |
|||
} |
|||
const deleteWallet = async walletId => { |
|||
console.log('deleteWallet', walletId) |
|||
await delay(200) |
|||
} |
|||
const setUnlockWalletError = async unlockWalletError => store.set({ unlockWalletError }) |
|||
const setActiveWallet = async activeWallet => store.set({ activeWallet }) |
|||
|
|||
storiesOf('Containers.Home', module) |
|||
.addParameters({ |
|||
info: { |
|||
disable: true |
|||
} |
|||
}) |
|||
.addDecorator( |
|||
StoryRouter({ |
|||
'/onboarding': linkTo('Containers.Onboarding', 'Onboarding'), |
|||
'/syncing': linkTo('Containers.Syncing', 'Syncing'), |
|||
'/app': linkTo('Containers.App', 'App') |
|||
}) |
|||
) |
|||
.add('Home', () => { |
|||
return ( |
|||
<Page css={{ height: 'calc(100vh - 40px)' }}> |
|||
<State store={store}> |
|||
<Home |
|||
startLnd={startLnd} |
|||
stopLnd={stopLnd} |
|||
unlockWallet={unlockWallet} |
|||
deleteWallet={deleteWallet} |
|||
setUnlockWalletError={setUnlockWalletError} |
|||
setActiveWallet={setActiveWallet} |
|||
/> |
|||
</State> |
|||
</Page> |
|||
) |
|||
}) |
Loading…
Reference in new issue