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