Browse Source
Add components for updated Pay form and build out reference use case in the storybook environment.renovate/lint-staged-8.x
Tom Kirkpatrick
6 years ago
22 changed files with 2138 additions and 6 deletions
@ -0,0 +1,702 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Box, Flex } from 'rebass' |
|||
import { animated, Keyframes, Transition } from 'react-spring' |
|||
import { FormattedMessage, injectIntl } from 'react-intl' |
|||
import lightningPayReq from 'bolt11' |
|||
import { getMinFee, getMaxFee, getFeeRange, isOnchain, isLn } from 'lib/utils/crypto' |
|||
import { convert } from 'lib/utils/btc' |
|||
import { |
|||
Bar, |
|||
CryptoAmountInput, |
|||
Dropdown, |
|||
FiatAmountInput, |
|||
Form, |
|||
FormFieldMessage, |
|||
Label, |
|||
LightningInvoiceInput, |
|||
Text |
|||
} from 'components/UI' |
|||
|
|||
import PayButtons from './PayButtons' |
|||
import PayHeader from './PayHeader' |
|||
import { PaySummaryLightning, PaySummaryOnChain } from '.' |
|||
import messages from './messages' |
|||
|
|||
/** |
|||
* Animation to handle showing/hiding the payReq field. |
|||
*/ |
|||
const ShowHidePayReq = Keyframes.Spring({ |
|||
small: { height: 48 }, |
|||
big: async (next, cancel, ownProps) => { |
|||
ownProps.context.focusPayReqInput() |
|||
await next({ height: 130 }) |
|||
} |
|||
}) |
|||
|
|||
/** |
|||
* Animation to handle showing/hiding the form buttons. |
|||
*/ |
|||
const ShowHideButtons = Keyframes.Spring({ |
|||
show: { opacity: 1 }, |
|||
hide: { opacity: 0 } |
|||
}) |
|||
|
|||
/** |
|||
* Animation to handle showing/hiding the amount fields. |
|||
*/ |
|||
const ShowHideAmount = Keyframes.Spring({ |
|||
show: async (next, cancel, ownProps) => { |
|||
await next({ display: 'block' }) |
|||
ownProps.context.focusAmountInput() |
|||
await next({ opacity: 1, height: 'auto' }) |
|||
}, |
|||
hide: { opacity: 0, height: 0, display: 'none' }, |
|||
remove: { opacity: 0, height: 0, display: 'none', immediate: true } |
|||
}) |
|||
|
|||
/** |
|||
* Payment form (onchain & offchain) |
|||
*/ |
|||
class Pay extends React.Component { |
|||
static propTypes = { |
|||
/** The currently active chain (bitcoin, litecoin etc) */ |
|||
chain: PropTypes.string.isRequired, |
|||
/** The currently active chain (mainnet, testnet) */ |
|||
network: PropTypes.string.isRequired, |
|||
/** Human readable chain name */ |
|||
cryptoName: PropTypes.string.isRequired, |
|||
/** Current channel balance (in satoshis). */ |
|||
channelBalance: PropTypes.number.isRequired, |
|||
/** Current ticker data as provided by blockchain.info */ |
|||
currentTicker: PropTypes.object.isRequired, |
|||
/** Currently selected cryptocurrency (key). */ |
|||
cryptoCurrency: PropTypes.string.isRequired, |
|||
/** Ticker symbol of the currently selected cryptocurrency. */ |
|||
cryptoCurrencyTicker: PropTypes.string.isRequired, |
|||
/** List of supported cryptocurrencies. */ |
|||
cryptoCurrencies: PropTypes.arrayOf( |
|||
PropTypes.shape({ |
|||
key: PropTypes.string.isRequired, |
|||
name: PropTypes.string.isRequired |
|||
}) |
|||
).isRequired, |
|||
/** List of supported fiat currencies. */ |
|||
fiatCurrencies: PropTypes.array.isRequired, |
|||
/** Currently selected fiat currency (key). */ |
|||
fiatCurrency: PropTypes.string.isRequired, |
|||
/** Payment address or invoice to populate the payReq field with when the form first loads. */ |
|||
initialPayReq: PropTypes.string, |
|||
/** Amount value to populate the amountCrypto field with when the form first loads. */ |
|||
initialAmountCrypto: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|||
/** Amount value to populate the amountFiat field with when the form first loads. */ |
|||
initialAmountFiat: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|||
/** Boolean indicating wether the form is being processed. If true, form buttons are disabled. */ |
|||
isProcessing: PropTypes.bool, |
|||
/** Boolean indicating wether fee information is currently being fetched. */ |
|||
isQueryingFees: PropTypes.bool, |
|||
/** Boolean indicating wether routing information is currently being fetched. */ |
|||
isQueryingRoutes: PropTypes.bool, |
|||
/** List of known nodes */ |
|||
nodes: PropTypes.array, |
|||
/** Current fee information as provided by bitcoinfees.earn.com */ |
|||
onchainFees: PropTypes.shape({ |
|||
fastestFee: PropTypes.number, |
|||
halfHourFee: PropTypes.number, |
|||
hourFee: PropTypes.number |
|||
}), |
|||
/** Routing information */ |
|||
routes: PropTypes.array, |
|||
/** Current wallet balance (in satoshis). */ |
|||
walletBalance: PropTypes.number.isRequired, |
|||
/** Method to process offChain invoice payments. Called when the form is submitted. */ |
|||
payInvoice: PropTypes.func.isRequired, |
|||
/** Set the current cryptocurrency. */ |
|||
setCryptoCurrency: PropTypes.func.isRequired, |
|||
/** Set the current fiat currency */ |
|||
setFiatCurrency: PropTypes.func.isRequired, |
|||
/** Method to process onChain transactions. Called when the form is submitted. */ |
|||
sendCoins: PropTypes.func.isRequired, |
|||
/** Method to fetch fee information for onchain transactions. */ |
|||
queryFees: PropTypes.func.isRequired, |
|||
/** Method to collect route information for lightning invoices. */ |
|||
queryRoutes: PropTypes.func.isRequired |
|||
} |
|||
|
|||
static defaultProps = { |
|||
initialAmountCrypto: null, |
|||
initialAmountFiat: null, |
|||
isProcessing: false, |
|||
isQueryingFees: false, |
|||
isQueryingRoutes: false, |
|||
nodes: [], |
|||
routes: [] |
|||
} |
|||
|
|||
state = { |
|||
currentStep: 'address', |
|||
isLn: null, |
|||
isOnchain: null |
|||
} |
|||
|
|||
amountInput = React.createRef() |
|||
payReqInput = React.createRef() |
|||
|
|||
/** |
|||
* If we have an address when the component mounts, run the payReq change handler to compure isLn / isOnchain. |
|||
*/ |
|||
componentDidMount() { |
|||
const { formApi } = this |
|||
if (formApi.getValue('payReq')) { |
|||
this.handlePayReqOnChange() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* If we have gone back to the address step, focus the address input and unmark all fields from being touched. |
|||
*/ |
|||
componentDidUpdate(prevProps, prevState) { |
|||
const { currentStep } = this.state |
|||
if (currentStep !== prevState.currentStep) { |
|||
if (currentStep === 'address') { |
|||
Object.keys(this.formApi.getState().touched).forEach(field => { |
|||
this.formApi.setTouched(field, false) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Form submit handler. |
|||
* @param {Object} values submitted form values. |
|||
*/ |
|||
onSubmit = values => { |
|||
const { currentStep, isOnchain } = this.state |
|||
const { cryptoCurrency, payInvoice, sendCoins } = this.props |
|||
if (currentStep === 'summary') { |
|||
return isOnchain |
|||
? sendCoins({ |
|||
value: values.amountCrypto, |
|||
addr: values.payReq, |
|||
currency: cryptoCurrency |
|||
}) |
|||
: payInvoice(values.payReq) |
|||
} else { |
|||
this.nextStep() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Store the formApi on the component context to make it available at this.formApi. |
|||
*/ |
|||
setFormApi = formApi => { |
|||
this.formApi = formApi |
|||
} |
|||
|
|||
/** |
|||
* set the amountFiat field. |
|||
*/ |
|||
setAmountFiat = () => { |
|||
if (this.amountInput.current) { |
|||
this.amountInput.current.focus() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Focus the payReq input. |
|||
*/ |
|||
focusPayReqInput = () => { |
|||
if (this.payReqInput.current) { |
|||
this.payReqInput.current.focus() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Focus the amount input. |
|||
*/ |
|||
focusAmountInput = () => { |
|||
if (this.amountInput.current) { |
|||
this.amountInput.current.focus() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Liost of enabled form steps. |
|||
*/ |
|||
steps = () => { |
|||
const { isLn, isOnchain } = this.state |
|||
let steps = ['address'] |
|||
if (isLn) { |
|||
steps = ['address', 'summary'] |
|||
} else if (isOnchain) { |
|||
steps = ['address', 'amount', 'summary'] |
|||
} |
|||
return steps |
|||
} |
|||
|
|||
/** |
|||
* Go back to previous form step. |
|||
*/ |
|||
previousStep = () => { |
|||
const { currentStep } = this.state |
|||
const nextStep = Math.max(this.steps().indexOf(currentStep) - 1, 0) |
|||
if (currentStep !== nextStep) { |
|||
this.setState({ currentStep: this.steps()[nextStep] }) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Progress to next form step. |
|||
*/ |
|||
nextStep = () => { |
|||
const { currentStep } = this.state |
|||
const nextStep = Math.min(this.steps().indexOf(currentStep) + 1, this.steps().length - 1) |
|||
if (currentStep !== nextStep) { |
|||
this.setState({ currentStep: this.steps()[nextStep] }) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Set isLn/isOnchain state based on payReq value. |
|||
*/ |
|||
handlePayReqOnChange = () => { |
|||
const { chain, network, queryRoutes } = this.props |
|||
const payReq = this.formApi.getValue('payReq') |
|||
const state = { |
|||
isLn: null, |
|||
isOnchain: null, |
|||
invoice: null |
|||
} |
|||
|
|||
// See if the user has entered a valid lightning payment request.
|
|||
if (isLn(payReq, chain, network)) { |
|||
let invoice |
|||
try { |
|||
invoice = lightningPayReq.decode(payReq) |
|||
state.invoice = invoice |
|||
} catch (e) { |
|||
return |
|||
} |
|||
state.isLn = true |
|||
const { satoshis, payeeNodeKey } = invoice |
|||
queryRoutes(payeeNodeKey, satoshis) |
|||
} |
|||
|
|||
// Otherwise, see if we have a valid onchain address.
|
|||
else if (isOnchain(payReq, chain, network)) { |
|||
state.isOnchain = true |
|||
} |
|||
|
|||
// Update the state with our findings.
|
|||
this.setState(state) |
|||
|
|||
// As soon as we have detected a valid address, submit the form.
|
|||
if (state.isLn || state.isOnchain) { |
|||
this.formApi.submitForm() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* set the amountFiat field whenever the crypto amount changes. |
|||
*/ |
|||
handleAmountCryptoChange = e => { |
|||
const { cryptoCurrency, currentTicker, fiatCurrency } = this.props |
|||
const lastPrice = currentTicker[fiatCurrency].last |
|||
const value = convert(cryptoCurrency, 'fiat', e.target.value, lastPrice) |
|||
this.formApi.setValue('amountFiat', value) |
|||
} |
|||
|
|||
/** |
|||
* set the amountCrypto field whenever the fiat amount changes. |
|||
*/ |
|||
handleAmountFiatChange = e => { |
|||
const { cryptoCurrency, currentTicker, fiatCurrency } = this.props |
|||
const lastPrice = currentTicker[fiatCurrency].last |
|||
const value = convert('fiat', cryptoCurrency, e.target.value, lastPrice) |
|||
this.formApi.setValue('amountCrypto', value) |
|||
} |
|||
|
|||
/** |
|||
* Handle changes from the crypto currency dropdown. |
|||
*/ |
|||
handleCryptoCurrencyChange = value => { |
|||
const { setCryptoCurrency } = this.props |
|||
setCryptoCurrency(value) |
|||
} |
|||
|
|||
/** |
|||
* Handle changes from the fiat currency dropdown. |
|||
*/ |
|||
handleFiatCurrencyChange = value => { |
|||
const { setFiatCurrency } = this.props |
|||
setFiatCurrency(value) |
|||
} |
|||
|
|||
renderHelpText = () => { |
|||
const { currentStep } = this.state |
|||
return ( |
|||
<Transition |
|||
native |
|||
items={currentStep === 'address'} |
|||
from={{ opacity: 0, height: 0 }} |
|||
enter={{ opacity: 1, height: 'auto' }} |
|||
leave={{ opacity: 0, height: 0 }} |
|||
initial={{ opacity: 1, height: 'auto' }} |
|||
> |
|||
{show => |
|||
show && |
|||
(styles => ( |
|||
<animated.div style={styles}> |
|||
<Box mb={5}> |
|||
<Text textAlign="justify"> |
|||
<FormattedMessage {...messages.description} /> |
|||
</Text> |
|||
</Box> |
|||
</animated.div> |
|||
)) |
|||
} |
|||
</Transition> |
|||
) |
|||
} |
|||
|
|||
renderAddressField = () => { |
|||
const { currentStep, isLn } = this.state |
|||
const { chain, initialPayReq, network } = this.props |
|||
return ( |
|||
<Box className={currentStep !== 'summary' ? 'element-show' : 'element-hide'}> |
|||
<Box pb={2}> |
|||
<Label htmlFor="payReq" readOnly={currentStep !== 'address'}> |
|||
{currentStep === 'address' ? ( |
|||
<FormattedMessage {...messages.request_label_combined} /> |
|||
) : isLn ? ( |
|||
<FormattedMessage {...messages.request_label_offchain} /> |
|||
) : ( |
|||
<FormattedMessage {...messages.request_label_onchain} /> |
|||
)} |
|||
</Label> |
|||
</Box> |
|||
|
|||
<ShowHidePayReq state={currentStep === 'address' ? 'big' : 'small'} context={this}> |
|||
{styles => ( |
|||
<React.Fragment> |
|||
<LightningInvoiceInput |
|||
style={styles} |
|||
initialValue={initialPayReq} |
|||
required |
|||
chain={chain} |
|||
network={network} |
|||
field="payReq" |
|||
validateOnBlur |
|||
validateOnChange |
|||
onChange={this.handlePayReqOnChange} |
|||
width={1} |
|||
readOnly={currentStep !== 'address'} |
|||
forwardedRef={this.payReqInput} |
|||
css={{ |
|||
resize: 'vertical', |
|||
'min-height': '48px' |
|||
}} |
|||
/> |
|||
</React.Fragment> |
|||
)} |
|||
</ShowHidePayReq> |
|||
</Box> |
|||
) |
|||
} |
|||
|
|||
renderAmountFields = () => { |
|||
const { currentStep } = this.state |
|||
const { |
|||
cryptoCurrency, |
|||
cryptoCurrencies, |
|||
currentTicker, |
|||
fiatCurrency, |
|||
fiatCurrencies, |
|||
initialAmountCrypto, |
|||
initialAmountFiat |
|||
} = this.props |
|||
return ( |
|||
<ShowHideAmount |
|||
state={currentStep === 'amount' ? 'show' : currentStep === 'address' ? 'hide' : 'remove'} |
|||
context={this} |
|||
> |
|||
{styles => ( |
|||
<Box style={styles}> |
|||
<Bar my={3} /> |
|||
<Label htmlFor="amountCrypto" pb={2}> |
|||
<FormattedMessage {...messages.amount} /> |
|||
</Label> |
|||
|
|||
<Flex justifyContent="space-between" alignItems="flex-start" mb={3}> |
|||
<Flex width={6 / 13}> |
|||
<Box width={145}> |
|||
<CryptoAmountInput |
|||
initialValue={initialAmountCrypto} |
|||
currency={cryptoCurrency} |
|||
required |
|||
field="amountCrypto" |
|||
width={145} |
|||
validateOnChange |
|||
validateOnBlur |
|||
onChange={this.handleAmountCryptoChange} |
|||
forwardedRef={this.amountInput} |
|||
disabled={currentStep === 'address'} |
|||
/> |
|||
</Box> |
|||
<Dropdown |
|||
activeKey={cryptoCurrency} |
|||
items={cryptoCurrencies} |
|||
onChange={this.handleCryptoCurrencyChange} |
|||
mt={2} |
|||
ml={2} |
|||
/> |
|||
</Flex> |
|||
<Text textAlign="center" mt={3} width={1 / 11}> |
|||
= |
|||
</Text> |
|||
<Flex width={6 / 13}> |
|||
<Box width={145} ml="auto"> |
|||
<FiatAmountInput |
|||
initialValue={initialAmountFiat} |
|||
currency={fiatCurrency} |
|||
currentTicker={currentTicker} |
|||
field="amountFiat" |
|||
width={145} |
|||
onChange={this.handleAmountFiatChange} |
|||
disabled={currentStep === 'address'} |
|||
/> |
|||
</Box> |
|||
|
|||
<Dropdown |
|||
activeKey={fiatCurrency} |
|||
items={fiatCurrencies} |
|||
onChange={this.handleFiatCurrencyChange} |
|||
mt={2} |
|||
ml={2} |
|||
/> |
|||
</Flex> |
|||
</Flex> |
|||
</Box> |
|||
)} |
|||
</ShowHideAmount> |
|||
) |
|||
} |
|||
|
|||
renderSummary = () => { |
|||
const { isOnchain } = this.state |
|||
const { |
|||
cryptoCurrency, |
|||
cryptoCurrencyTicker, |
|||
cryptoCurrencies, |
|||
currentTicker, |
|||
fiatCurrency, |
|||
isQueryingFees, |
|||
isQueryingRoutes, |
|||
nodes, |
|||
onchainFees, |
|||
queryFees, |
|||
routes, |
|||
setCryptoCurrency |
|||
} = this.props |
|||
const formState = this.formApi.getState() |
|||
let minFee, maxFee |
|||
if (routes.length) { |
|||
minFee = getMinFee(routes) |
|||
maxFee = getMaxFee(routes) |
|||
} |
|||
|
|||
// convert entered amount to satoshis
|
|||
if (isOnchain) { |
|||
const amountInSatoshis = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) |
|||
return ( |
|||
<PaySummaryOnChain |
|||
amount={amountInSatoshis} |
|||
address={formState.values.payReq} |
|||
cryptoCurrency={cryptoCurrency} |
|||
cryptoCurrencyTicker={cryptoCurrencyTicker} |
|||
cryptoCurrencies={cryptoCurrencies} |
|||
currentTicker={currentTicker} |
|||
setCryptoCurrency={setCryptoCurrency} |
|||
fiatCurrency={fiatCurrency} |
|||
isQueryingFees={isQueryingFees} |
|||
onchainFees={onchainFees} |
|||
queryFees={queryFees} |
|||
/> |
|||
) |
|||
} else if (isLn) { |
|||
return ( |
|||
<PaySummaryLightning |
|||
currentTicker={currentTicker} |
|||
cryptoCurrency={cryptoCurrency} |
|||
cryptoCurrencyTicker={cryptoCurrencyTicker} |
|||
cryptoCurrencies={cryptoCurrencies} |
|||
fiatCurrency={fiatCurrency} |
|||
isQueryingRoutes={isQueryingRoutes} |
|||
minFee={minFee} |
|||
maxFee={maxFee} |
|||
nodes={nodes} |
|||
payReq={formState.values.payReq} |
|||
setCryptoCurrency={setCryptoCurrency} |
|||
/> |
|||
) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Form renderer. |
|||
*/ |
|||
render() { |
|||
const { currentStep, invoice, isLn, isOnchain } = this.state |
|||
const { |
|||
chain, |
|||
network, |
|||
channelBalance, |
|||
cryptoCurrency, |
|||
cryptoCurrencyTicker, |
|||
cryptoCurrencies, |
|||
currentTicker, |
|||
cryptoName, |
|||
fiatCurrencies, |
|||
fiatCurrency, |
|||
initialPayReq, |
|||
initialAmountCrypto, |
|||
initialAmountFiat, |
|||
intl, |
|||
isProcessing, |
|||
isQueryingFees, |
|||
isQueryingRoutes, |
|||
onchainFees, |
|||
payInvoice, |
|||
sendCoins, |
|||
setCryptoCurrency, |
|||
setFiatCurrency, |
|||
queryFees, |
|||
queryRoutes, |
|||
routes, |
|||
walletBalance, |
|||
...rest |
|||
} = this.props |
|||
return ( |
|||
<Form |
|||
width={1} |
|||
css={{ height: '100%' }} |
|||
{...rest} |
|||
getApi={this.setFormApi} |
|||
onSubmit={this.onSubmit} |
|||
> |
|||
{({ formState }) => { |
|||
// Deterine which buttons should be visible.
|
|||
const showBack = currentStep !== 'address' |
|||
const showSubmit = currentStep !== 'address' || (isOnchain || isLn) |
|||
|
|||
// Determine wether we have a route to the sender.
|
|||
let hasRoute = true |
|||
if (isLn && currentStep === 'summary') { |
|||
const { min, max } = getFeeRange(routes || []) |
|||
if (min === null || max === null) { |
|||
hasRoute = false |
|||
} |
|||
} |
|||
|
|||
// Determine wether we have enough funds available.
|
|||
let hasEnoughFunds = true |
|||
if (isLn && invoice) { |
|||
hasEnoughFunds = invoice.satoshis <= channelBalance |
|||
} else if (isOnchain) { |
|||
const valueInSats = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) |
|||
hasEnoughFunds = valueInSats <= walletBalance |
|||
} |
|||
|
|||
// Determine what the text should be for the next button.
|
|||
let nextButtonText = intl.formatMessage({ ...messages.next }) |
|||
if (currentStep === 'summary') { |
|||
const value = |
|||
isLn && invoice |
|||
? convert('sats', cryptoCurrency, invoice.satoshis) |
|||
: formState.values.amountCrypto |
|||
nextButtonText = `${intl.formatMessage({ |
|||
...messages.send |
|||
})} ${value} ${cryptoCurrencyTicker}` |
|||
} |
|||
|
|||
return ( |
|||
<Flex as="article" flexDirection="column" css={{ height: '100%' }}> |
|||
<Flex as="header" flexDirection="column" mb={2}> |
|||
<PayHeader |
|||
title={`${intl.formatMessage({ |
|||
...messages.send |
|||
})} ${cryptoName} (${cryptoCurrencyTicker})`}
|
|||
type={isLn ? 'offchain' : isOnchain ? 'onchain' : null} |
|||
/> |
|||
<Bar /> |
|||
</Flex> |
|||
|
|||
<Box as="section" css={{ flex: 1 }} mb={3}> |
|||
{this.renderHelpText()} |
|||
{this.renderAddressField()} |
|||
{isOnchain && this.renderAmountFields()} |
|||
{currentStep === 'summary' && this.renderSummary()} |
|||
</Box> |
|||
|
|||
<Box as="footer" mt="auto"> |
|||
<ShowHideButtons state={showBack || showSubmit ? 'show' : 'show'}> |
|||
{styles => ( |
|||
<Box style={styles}> |
|||
{currentStep === 'summary' && |
|||
!isQueryingRoutes && |
|||
!hasRoute && ( |
|||
<FormFieldMessage variant="error" justifyContent="center" mb={2}> |
|||
<FormattedMessage {...messages.error_no_route} /> |
|||
</FormFieldMessage> |
|||
)} |
|||
|
|||
{currentStep === 'summary' && |
|||
!hasEnoughFunds && ( |
|||
<FormFieldMessage variant="error" justifyContent="center" mb={2}> |
|||
<FormattedMessage {...messages.error_not_enough_funds} /> |
|||
</FormFieldMessage> |
|||
)} |
|||
|
|||
<PayButtons |
|||
disabled={ |
|||
formState.pristine || |
|||
formState.invalid || |
|||
(currentStep === 'summary' && (!hasRoute || !hasEnoughFunds)) |
|||
} |
|||
nextButtonText={nextButtonText} |
|||
processing={isProcessing} |
|||
showBack={showBack} |
|||
showSubmit={showSubmit} |
|||
previousStep={this.previousStep} |
|||
/> |
|||
|
|||
{walletBalance !== null && ( |
|||
<React.Fragment> |
|||
<Text textAlign="center" mt={3} fontWeight="normal"> |
|||
<FormattedMessage {...messages.current_balance} />: |
|||
</Text> |
|||
<Text textAlign="center" fontSize="xs"> |
|||
{convert('sats', cryptoCurrency, walletBalance)} |
|||
{` `} |
|||
{cryptoCurrencyTicker} (onchain), |
|||
</Text> |
|||
<Text textAlign="center" fontSize="xs"> |
|||
{convert('sats', cryptoCurrency, channelBalance)} |
|||
{` `} |
|||
{cryptoCurrencyTicker} (in channels) |
|||
</Text> |
|||
</React.Fragment> |
|||
)} |
|||
</Box> |
|||
)} |
|||
</ShowHideButtons> |
|||
</Box> |
|||
</Flex> |
|||
) |
|||
}} |
|||
</Form> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default injectIntl(Pay) |
@ -0,0 +1,74 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FormattedMessage } from 'react-intl' |
|||
import { Box, Flex, Text } from 'rebass' |
|||
import BigArrowLeft from 'components/Icon/BigArrowLeft' |
|||
import { Button } from 'components/UI' |
|||
import messages from './messages' |
|||
|
|||
/** |
|||
* Buttons for Pay. |
|||
*/ |
|||
class PayButtons extends React.PureComponent { |
|||
static propTypes = { |
|||
disabled: PropTypes.bool, |
|||
nextButtonText: PropTypes.node, |
|||
previousStep: PropTypes.func, |
|||
processing: PropTypes.bool, |
|||
showBack: PropTypes.bool, |
|||
showSubmit: PropTypes.bool |
|||
} |
|||
|
|||
static defaultProps = { |
|||
disabled: false, |
|||
nextButtonText: <FormattedMessage {...messages.next} />, |
|||
previousStep: () => ({}), |
|||
processing: false, |
|||
showBack: true, |
|||
showSubmit: true |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
disabled, |
|||
nextButtonText, |
|||
previousStep, |
|||
processing, |
|||
showBack, |
|||
showSubmit, |
|||
...rest |
|||
} = this.props |
|||
return ( |
|||
<Flex {...rest} justifyContent="space-between" alignItems="center"> |
|||
<Box width={1 / 5}> |
|||
{showBack && ( |
|||
<Button |
|||
type="button" |
|||
variant="secondary" |
|||
onClick={previousStep} |
|||
px={0} |
|||
disabled={processing} |
|||
> |
|||
<Flex> |
|||
<Text> |
|||
<BigArrowLeft /> |
|||
</Text> |
|||
<Text ml={1}> |
|||
<FormattedMessage {...messages.back} /> |
|||
</Text> |
|||
</Flex> |
|||
</Button> |
|||
)} |
|||
</Box> |
|||
{showSubmit && ( |
|||
<Button type="submit" mx="auto" disabled={disabled || processing} processing={processing}> |
|||
{nextButtonText} |
|||
</Button> |
|||
)} |
|||
<Box width={1 / 5} /> |
|||
</Flex> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default PayButtons |
@ -0,0 +1,37 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Box } from 'rebass' |
|||
import { Heading, Text } from 'components/UI' |
|||
import Lightning from 'components/Icon/Lightning' |
|||
import Onchain from 'components/Icon/Onchain' |
|||
import PaperPlane from 'components/Icon/PaperPlane' |
|||
|
|||
/** |
|||
* Header for opayment form. |
|||
*/ |
|||
class PayHeader extends React.PureComponent { |
|||
static propTypes = { |
|||
title: PropTypes.string.isRequired, |
|||
type: PropTypes.oneOf(['onchain', 'offchain']) |
|||
} |
|||
|
|||
render() { |
|||
const { title, type } = this.props |
|||
return ( |
|||
<Text textAlign="center"> |
|||
<Box mx="auto" css={{ height: '55px' }}> |
|||
{type === 'offchain' && <Lightning height="45px" width="45px" />} |
|||
{type === 'onchain' && <Onchain height="45px" width="45px" />} |
|||
{!type && <PaperPlane height="35px" width="35px" />} |
|||
</Box> |
|||
<Heading.h1 mx="auto">{title}</Heading.h1> |
|||
<Heading.h4 mx="auto"> |
|||
|
|||
{type === 'onchain' && 'On-Chain Payment'} {type === 'offchain' && 'Lightning Payment'} |
|||
</Heading.h4> |
|||
</Text> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default PayHeader |
@ -0,0 +1,163 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Box, Flex } from 'rebass' |
|||
import { FormattedNumber, FormattedMessage } from 'react-intl' |
|||
import lightningPayReq from 'bolt11' |
|||
import { satoshisToFiat } from 'lib/utils/btc' |
|||
import { getNodeAlias } from 'lib/utils/crypto' |
|||
import BigArrowRight from 'components/Icon/BigArrowRight' |
|||
import { Bar, Dropdown, Spinner, Text, Truncate } from 'components/UI' |
|||
import Value from 'components/Value' |
|||
import { PaySummaryRow } from '.' |
|||
import messages from './messages' |
|||
|
|||
class PaySummaryLightning extends React.PureComponent { |
|||
static propTypes = { |
|||
/** Current ticker data as provided by blockchain.info */ |
|||
currentTicker: PropTypes.object.isRequired, |
|||
/** Currently selected cryptocurrency (key). */ |
|||
cryptoCurrency: PropTypes.string.isRequired, |
|||
/** Ticker symbol of the currently selected cryptocurrency. */ |
|||
cryptoCurrencyTicker: PropTypes.string.isRequired, |
|||
/** List of supported cryptocurrencies. */ |
|||
cryptoCurrencies: PropTypes.arrayOf( |
|||
PropTypes.shape({ |
|||
key: PropTypes.string.isRequired, |
|||
name: PropTypes.string.isRequired |
|||
}) |
|||
).isRequired, |
|||
/** Currently selected fiat currency (key). */ |
|||
fiatCurrency: PropTypes.string.isRequired, |
|||
/** Boolean indicating wether routing information is currently being fetched. */ |
|||
isQueryingRoutes: PropTypes.bool, |
|||
/** Maximum fee for the payment */ |
|||
maxFee: PropTypes.number, |
|||
/** Minimumfee for the payment */ |
|||
minFee: PropTypes.number, |
|||
/** List of nodes as returned by lnd */ |
|||
nodes: PropTypes.array, |
|||
/** Lightning Payment request */ |
|||
payReq: PropTypes.string.isRequired, |
|||
|
|||
/** Set the current cryptocurrency. */ |
|||
setCryptoCurrency: PropTypes.func.isRequired |
|||
} |
|||
|
|||
static defaultProps = { |
|||
isQueryingRoutes: false, |
|||
minFee: null, |
|||
maxFee: null, |
|||
nodes: [] |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
cryptoCurrency, |
|||
cryptoCurrencyTicker, |
|||
cryptoCurrencies, |
|||
currentTicker, |
|||
fiatCurrency, |
|||
isQueryingRoutes, |
|||
maxFee, |
|||
minFee, |
|||
nodes, |
|||
payReq, |
|||
setCryptoCurrency |
|||
} = this.props |
|||
|
|||
let invoice |
|||
try { |
|||
invoice = lightningPayReq.decode(payReq) |
|||
} catch (e) { |
|||
return null |
|||
} |
|||
|
|||
const { satoshis, payeeNodeKey } = invoice |
|||
const descriptionTag = invoice.tags.find(tag => tag.tagName === 'description') || {} |
|||
const memo = descriptionTag.data |
|||
const fiatAmount = satoshisToFiat(satoshis, currentTicker[fiatCurrency].last) |
|||
const nodeAlias = getNodeAlias(payeeNodeKey, nodes) |
|||
|
|||
return ( |
|||
<React.Fragment> |
|||
<Box pb={2}> |
|||
<Flex alignItems="center"> |
|||
<Box width={5 / 11}> |
|||
<Flex flexWrap="wrap" alignItems="baseline"> |
|||
<Box> |
|||
<Text textAlign="left" fontSize={6}> |
|||
<Value value={satoshis} currency={cryptoCurrency} /> |
|||
</Text> |
|||
</Box> |
|||
<Dropdown |
|||
activeKey={cryptoCurrency} |
|||
items={cryptoCurrencies} |
|||
onChange={setCryptoCurrency} |
|||
ml={2} |
|||
/> |
|||
</Flex> |
|||
<Text color="gray"> |
|||
{'≈ '} |
|||
<FormattedNumber currency={fiatCurrency} style="currency" value={fiatAmount} /> |
|||
</Text> |
|||
</Box> |
|||
<Box width={1 / 11}> |
|||
<Text textAlign="center" color="lightningOrange"> |
|||
<BigArrowRight width="40px" height="28px" /> |
|||
</Text> |
|||
</Box> |
|||
<Box width={5 / 11}> |
|||
<Text textAlign="right" className="hint--bottom-left" data-hint={payeeNodeKey}> |
|||
{<Truncate text={nodeAlias || payeeNodeKey} />} |
|||
</Text> |
|||
</Box> |
|||
</Flex> |
|||
</Box> |
|||
|
|||
<Bar /> |
|||
|
|||
<PaySummaryRow |
|||
left={<FormattedMessage {...messages.fee} />} |
|||
right={ |
|||
isQueryingRoutes ? ( |
|||
<Flex ml="auto" alignItems="center" justifyContent="flex-end"> |
|||
<Text mr={2}> |
|||
<FormattedMessage {...messages.searching_routes} /> |
|||
… |
|||
</Text> |
|||
<Spinner color="lightningOrange" /> |
|||
</Flex> |
|||
) : minFee === null || maxFee === null ? ( |
|||
<FormattedMessage {...messages.unknown} /> |
|||
) : ( |
|||
<FormattedMessage {...messages.fee_range} values={{ minFee, maxFee }} /> |
|||
) |
|||
} |
|||
/> |
|||
|
|||
<Bar /> |
|||
|
|||
<PaySummaryRow |
|||
left={<FormattedMessage {...messages.total} />} |
|||
right={ |
|||
<React.Fragment> |
|||
<Value value={satoshis} currency={cryptoCurrency} /> {cryptoCurrencyTicker} |
|||
{!isQueryingRoutes && |
|||
maxFee && ( |
|||
<Text fontSize="s"> |
|||
(+ <FormattedMessage {...messages.upto} /> {maxFee} msats) |
|||
</Text> |
|||
)} |
|||
</React.Fragment> |
|||
} |
|||
/> |
|||
|
|||
<Bar /> |
|||
|
|||
{memo && <PaySummaryRow left={<FormattedMessage {...messages.memo} />} right={memo} />} |
|||
</React.Fragment> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default PaySummaryLightning |
@ -0,0 +1,155 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Box, Flex } from 'rebass' |
|||
import { FormattedNumber, FormattedMessage } from 'react-intl' |
|||
import get from 'lodash.get' |
|||
import { satoshisToFiat } from 'lib/utils/btc' |
|||
import BigArrowRight from 'components/Icon/BigArrowRight' |
|||
import { Bar, Dropdown, Spinner, Text, Truncate } from 'components/UI' |
|||
import Value from 'components/Value' |
|||
import { PaySummaryRow } from '.' |
|||
import messages from './messages' |
|||
|
|||
class PaySummaryOnChain extends React.Component { |
|||
static propTypes = { |
|||
/** Amount to send (in satoshis). */ |
|||
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, |
|||
/** Onchain address of recipient. */ |
|||
address: PropTypes.string.isRequired, |
|||
/** Currently selected cryptocurrency (key). */ |
|||
cryptoCurrency: PropTypes.string.isRequired, |
|||
/** Ticker symbol of the currently selected cryptocurrency. */ |
|||
cryptoCurrencyTicker: PropTypes.string.isRequired, |
|||
/** List of supported cryptocurrencies. */ |
|||
cryptoCurrencies: PropTypes.arrayOf( |
|||
PropTypes.shape({ |
|||
key: PropTypes.string.isRequired, |
|||
name: PropTypes.string.isRequired |
|||
}) |
|||
).isRequired, |
|||
/** Current ticker data as provided by blockchain.info */ |
|||
currentTicker: PropTypes.object.isRequired, |
|||
/** Current fee information as provided by bitcoinfees.earn.com */ |
|||
onchainFees: PropTypes.shape({ |
|||
fastestFee: PropTypes.number, |
|||
halfHourFee: PropTypes.number, |
|||
hourFee: PropTypes.number |
|||
}), |
|||
/** Currently selected fiat currency (key). */ |
|||
fiatCurrency: PropTypes.string.isRequired, |
|||
/** Boolean indicating wether routing information is currently being fetched. */ |
|||
isQueryingFees: PropTypes.bool, |
|||
|
|||
/** Method to fetch fee information for onchain transactions. */ |
|||
queryFees: PropTypes.func.isRequired, |
|||
/** Set the current cryptocurrency. */ |
|||
setCryptoCurrency: PropTypes.func.isRequired |
|||
} |
|||
|
|||
static defaultProps = { |
|||
isQueryingFees: false, |
|||
onchainFees: {} |
|||
} |
|||
|
|||
componenDidMount() { |
|||
const { queryFees } = this.props |
|||
queryFees() |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
amount, |
|||
address, |
|||
cryptoCurrency, |
|||
cryptoCurrencyTicker, |
|||
cryptoCurrencies, |
|||
currentTicker, |
|||
fiatCurrency, |
|||
onchainFees, |
|||
isQueryingFees, |
|||
setCryptoCurrency |
|||
} = this.props |
|||
|
|||
const fiatAmount = satoshisToFiat(amount, currentTicker[fiatCurrency].last) |
|||
const fee = get(onchainFees, 'fastestFee', null) |
|||
return ( |
|||
<React.Fragment> |
|||
<Box pb={2}> |
|||
<Flex alignItems="center"> |
|||
<Box width={5 / 11}> |
|||
<Flex flexWrap="wrap" alignItems="baseline"> |
|||
<Box> |
|||
<Text textAlign="left" fontSize={6}> |
|||
<Value value={amount} currency={cryptoCurrency} /> |
|||
</Text> |
|||
</Box> |
|||
<Dropdown |
|||
activeKey={cryptoCurrency} |
|||
items={cryptoCurrencies} |
|||
onChange={setCryptoCurrency} |
|||
ml={2} |
|||
/> |
|||
</Flex> |
|||
<Text color="gray"> |
|||
{' ≈ '} |
|||
<FormattedNumber currency={fiatCurrency} style="currency" value={fiatAmount} /> |
|||
</Text> |
|||
</Box> |
|||
<Box width={1 / 11}> |
|||
<Text textAlign="center" color="lightningOrange"> |
|||
<BigArrowRight width="40px" height="28px" /> |
|||
</Text> |
|||
</Box> |
|||
<Box width={5 / 11}> |
|||
<Text textAlign="right" className="hint--bottom-left" data-hint={address}> |
|||
<Truncate text={address} /> |
|||
</Text> |
|||
</Box> |
|||
</Flex> |
|||
</Box> |
|||
|
|||
<Bar /> |
|||
|
|||
<PaySummaryRow |
|||
left={<FormattedMessage {...messages.fee} />} |
|||
right={ |
|||
isQueryingFees ? ( |
|||
<Flex ml="auto" alignItems="center" justifyContent="flex-end"> |
|||
<Text mr={2}> |
|||
<FormattedMessage {...messages.calculating} /> |
|||
… |
|||
</Text> |
|||
<Spinner color="lightningOrange" /> |
|||
</Flex> |
|||
) : !fee ? ( |
|||
<FormattedMessage {...messages.unknown} /> |
|||
) : ( |
|||
<React.Fragment> |
|||
<Text> |
|||
{fee} satoshis <FormattedMessage {...messages.per_byte} /> |
|||
</Text> |
|||
<Text fontSize="s"> |
|||
(<FormattedMessage {...messages.next_block_confirmation} />) |
|||
</Text> |
|||
</React.Fragment> |
|||
) |
|||
} |
|||
/> |
|||
|
|||
<Bar /> |
|||
|
|||
<PaySummaryRow |
|||
left={<FormattedMessage {...messages.total} />} |
|||
right={ |
|||
<React.Fragment> |
|||
<Value value={amount} currency={cryptoCurrency} /> {cryptoCurrencyTicker} |
|||
{!isQueryingFees && fee && <Text fontSize="s">(+ {fee} satoshis per byte</Text>} |
|||
</React.Fragment> |
|||
} |
|||
/> |
|||
</React.Fragment> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default PaySummaryOnChain |
@ -0,0 +1,24 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { Box, Flex } from 'rebass' |
|||
import { Text } from 'components/UI' |
|||
|
|||
const PaySummaryRow = ({ left, right }) => ( |
|||
<Box py={2}> |
|||
<Flex alignItems="center"> |
|||
<Box width={1 / 2}> |
|||
<Text fontWeight="normal">{left}</Text> |
|||
</Box> |
|||
<Box width={1 / 2}> |
|||
<Text textAlign="right">{right}</Text> |
|||
</Box> |
|||
</Flex> |
|||
</Box> |
|||
) |
|||
|
|||
PaySummaryRow.propTypes = { |
|||
left: PropTypes.any, |
|||
right: PropTypes.any |
|||
} |
|||
|
|||
export default PaySummaryRow |
@ -0,0 +1,6 @@ |
|||
export Pay from './Pay' |
|||
export PayButtons from './PayButtons' |
|||
export PayHeader from './PayHeader' |
|||
export PaySummaryLightning from './PaySummaryLightning' |
|||
export PaySummaryOnChain from './PaySummaryOnChain' |
|||
export PaySummaryRow from './PaySummaryRow' |
@ -0,0 +1,27 @@ |
|||
import { defineMessages } from 'react-intl' |
|||
|
|||
/* eslint-disable max-len */ |
|||
export default defineMessages({ |
|||
calculating: 'calculating', |
|||
current_balance: 'Your current balance', |
|||
error_no_route: 'We were unable to find a route to your destination. Please try again later.', |
|||
error_not_enough_funds: 'You do not have enough funds available to make this payment.', |
|||
request_label_combined: 'Payment Request or Address', |
|||
request_label_offchain: 'Payment Request', |
|||
request_label_onchain: 'Address', |
|||
searching_routes: 'searching for routes', |
|||
next_block_confirmation: 'next block confirmation', |
|||
next: 'Next', |
|||
back: 'Back', |
|||
send: 'Send', |
|||
fee: 'Fee', |
|||
fee_range: 'between {minFee} and {maxFee} msat', |
|||
unknown: 'unknown', |
|||
amount: 'Amount', |
|||
per_byte: 'per byte', |
|||
upto: 'up to', |
|||
total: 'Total', |
|||
memo: 'Memo', |
|||
description: |
|||
'You can send Bitcoin (BTC) through the Lightning Network or make a On-Chain Transaction. Just paste your Lightning Payment Request or the Bitcoin Address in the field below. Zap will guide you to the process.' |
|||
}) |
@ -0,0 +1,296 @@ |
|||
/* eslint-disable max-len */ |
|||
|
|||
import React from 'react' |
|||
import { storiesOf } from '@storybook/react' |
|||
import { action } from '@storybook/addon-actions' |
|||
import { number, select, text } from '@storybook/addon-knobs' |
|||
import { State, Store } from '@sambego/storybook-state' |
|||
import { Modal, Page } from 'components/UI' |
|||
import { Pay, PayButtons, PayHeader, PaySummaryLightning, PaySummaryOnChain } from 'components/Pay' |
|||
|
|||
const delay = time => new Promise(resolve => setTimeout(() => resolve(), time)) |
|||
|
|||
const store = new Store({ |
|||
chain: 'bitcoin', |
|||
network: 'testnet', |
|||
cryptoName: 'Bitcoin', |
|||
walletBalance: 10000000, |
|||
channelBalance: 25000, |
|||
cryptoCurrency: 'btc', |
|||
cryptoCurrencyTicker: 'BTC', |
|||
cryptoCurrencies: [ |
|||
{ |
|||
key: 'btc', |
|||
name: 'BTC' |
|||
}, |
|||
{ |
|||
key: 'bits', |
|||
name: 'bits' |
|||
}, |
|||
{ |
|||
key: 'sats', |
|||
name: 'satoshis' |
|||
} |
|||
], |
|||
|
|||
fiatCurrency: 'USD', |
|||
fiatCurrencies: ['USD', 'EUR', 'GBP'], |
|||
|
|||
nodes: [ |
|||
{ |
|||
pub_key: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463', |
|||
alias: 'htlc.me' |
|||
} |
|||
], |
|||
routes: [], |
|||
|
|||
currentTicker: { |
|||
USD: { |
|||
last: 6477.78 |
|||
}, |
|||
EUR: { |
|||
last: 5656.01 |
|||
}, |
|||
GBP: { |
|||
last: 5052.73 |
|||
} |
|||
}, |
|||
|
|||
isProcessing: false |
|||
}) |
|||
|
|||
const mockPayInvoice = async () => { |
|||
store.set({ isProcessing: true }) |
|||
await delay(2000) |
|||
store.set({ isProcessing: false }) |
|||
} |
|||
|
|||
const mockSendCoins = async () => { |
|||
store.set({ isProcessing: true }) |
|||
await delay(2000) |
|||
store.set({ isProcessing: false }) |
|||
} |
|||
|
|||
const mockQueryFees = async () => { |
|||
store.set({ isQueryingFees: true }) |
|||
await delay(2000) |
|||
store.set({ |
|||
onchainFees: { |
|||
fastestFee: 8, |
|||
halfHourFee: 8, |
|||
hourFee: 4 |
|||
} |
|||
}) |
|||
store.set({ isQueryingFees: false }) |
|||
} |
|||
|
|||
const mockQueryRoutes = async pubKey => { |
|||
store.set({ isQueryingRoutes: true }) |
|||
await delay(2000) |
|||
const nodes = store.get('nodes') |
|||
if (nodes.find(n => n.pub_key === pubKey)) { |
|||
store.set({ |
|||
routes: [ |
|||
{ |
|||
total_time_lock: 547118, |
|||
total_fees: '0', |
|||
total_amt: '10000', |
|||
hops: [ |
|||
{ |
|||
chan_id: '565542601916153857', |
|||
chan_capacity: '15698', |
|||
amt_to_forward: '10000', |
|||
fee: '0', |
|||
expiry: 546974, |
|||
amt_to_forward_msat: '10000010', |
|||
fee_msat: '21' |
|||
} |
|||
], |
|||
total_fees_msat: '21', |
|||
total_amt_msat: '10000021' |
|||
}, |
|||
{ |
|||
total_time_lock: 547118, |
|||
total_fees: '0', |
|||
total_amt: '10000', |
|||
hops: [ |
|||
{ |
|||
chan_id: '565542601916153857', |
|||
chan_capacity: '15698', |
|||
amt_to_forward: '10000', |
|||
fee: '0', |
|||
expiry: 546974, |
|||
amt_to_forward_msat: '10000010', |
|||
fee_msat: '3' |
|||
} |
|||
], |
|||
total_fees_msat: '3', |
|||
total_amt_msat: '10000021' |
|||
} |
|||
] |
|||
}) |
|||
} else { |
|||
store.set({ routes: [] }) |
|||
} |
|||
store.set({ isQueryingRoutes: false }) |
|||
} |
|||
|
|||
const setCryptoCurrency = key => { |
|||
const items = store.get('cryptoCurrencies') |
|||
const item = items.find(i => i.key === key) |
|||
store.set({ cryptoCurrency: item.key }) |
|||
store.set({ cryptoCurrencyTicker: item.name }) |
|||
} |
|||
|
|||
const setFiatCurrency = key => { |
|||
store.set({ fiatCurrency: key }) |
|||
} |
|||
|
|||
storiesOf('Containers.Pay', module) |
|||
.add('Pay', () => { |
|||
const network = select( |
|||
'Network', |
|||
{ |
|||
Testnet: 'testnet', |
|||
Mainnet: 'mainnet' |
|||
}, |
|||
'testnet' |
|||
) |
|||
|
|||
return ( |
|||
<Page css={{ height: 'calc(100vh - 40px)' }}> |
|||
<Modal onClose={action('clicked')}> |
|||
<State store={store}> |
|||
<Pay |
|||
width={1 / 2} |
|||
mx="auto" |
|||
// State
|
|||
isProcessing={store.get('isProcessing')} |
|||
chain={store.get('chain')} |
|||
channelBalance={store.get('channelBalance')} |
|||
network={network} |
|||
cryptoCurrency={store.get('cryptoCurrency')} |
|||
cryptoCurrencyTicker={store.get('cryptoCurrencyTicker')} |
|||
cryptoCurrencies={store.get('cryptoCurrencies')} |
|||
currentTicker={store.get('currentTicker')} |
|||
cryptoName={store.get('cryptoName')} |
|||
fiatCurrency={store.get('fiatCurrency')} |
|||
fiatCurrencies={store.get('fiatCurrencies')} |
|||
isQueryingFees={store.get('isQueryingFees')} |
|||
isQueryingRoutes={store.get('isQueryingRoutes')} |
|||
nodes={store.get('nodes')} |
|||
walletBalance={store.get('walletBalance')} |
|||
// Dispatch
|
|||
payInvoice={mockPayInvoice} |
|||
setCryptoCurrency={setCryptoCurrency} |
|||
setFiatCurrency={setFiatCurrency} |
|||
sendCoins={mockSendCoins} |
|||
queryFees={mockQueryFees} |
|||
queryRoutes={mockQueryRoutes} |
|||
/> |
|||
</State> |
|||
</Modal> |
|||
</Page> |
|||
) |
|||
}) |
|||
.addWithChapters('PayHeader', { |
|||
chapters: [ |
|||
{ |
|||
sections: [ |
|||
{ |
|||
title: 'On-chain', |
|||
sectionFn: () => <PayHeader title="Send Bitcoin" type="onchain" /> |
|||
}, |
|||
{ |
|||
title: 'Off-chain', |
|||
sectionFn: () => <PayHeader title="Send Bitcoin" type="offchain" /> |
|||
}, |
|||
{ |
|||
title: 'Generic', |
|||
sectionFn: () => <PayHeader title="Send Bitcoin" /> |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}) |
|||
.addWithChapters('PaySummary', { |
|||
chapters: [ |
|||
{ |
|||
sections: [ |
|||
{ |
|||
title: 'PaySummaryLightning', |
|||
sectionFn: () => ( |
|||
<PaySummaryLightning |
|||
// State
|
|||
cryptoCurrency={store.get('cryptoCurrency')} |
|||
cryptoCurrencyTicker={store.get('cryptoCurrencyTicker')} |
|||
cryptoCurrencies={store.get('cryptoCurrencies')} |
|||
currentTicker={store.get('currentTicker')} |
|||
fiatCurrency={store.get('fiatCurrency')} |
|||
fiatCurrencies={store.get('fiatCurrencies')} |
|||
minFee={12} |
|||
maxFee={18} |
|||
nodes={store.get('nodes')} |
|||
payReq={text( |
|||
'Lightning Invoice', |
|||
'lntb100u1pdaxza7pp5x73t3j7xgvkzgcdvzgpdg74k4pn0uhwuxlxu9qssytjn77x7zs4qdqqcqzysxqyz5vqd20eaq5uferzgzwasu5te3pla7gv8tzk8gcdxlj7lpkygvfdwndhwtl3ezn9ltjejl3hsp36ps3z3e5pp4rzp2hgqjqql80ec3hyzucq4d9axl' |
|||
)} |
|||
// Dispatch
|
|||
setCryptoCurrency={setCryptoCurrency} |
|||
setFiatCurrency={setFiatCurrency} |
|||
/> |
|||
), |
|||
options: { |
|||
decorator: story => <State store={store}>{story()}</State> |
|||
} |
|||
}, |
|||
{ |
|||
title: 'PaySummaryOnChain', |
|||
sectionFn: () => { |
|||
mockQueryFees() |
|||
return ( |
|||
<PaySummaryOnChain |
|||
// State
|
|||
address={text('Address', 'mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c')} |
|||
amount={number('Amount (satoshis)', 10000)} |
|||
cryptoCurrency={store.get('cryptoCurrency')} |
|||
cryptoCurrencyTicker={store.get('cryptoCurrencyTicker')} |
|||
cryptoCurrencies={store.get('cryptoCurrencies')} |
|||
currentTicker={store.get('currentTicker')} |
|||
fiatCurrency={store.get('fiatCurrency')} |
|||
fiatCurrencies={store.get('fiatCurrencies')} |
|||
onchainFees={store.get('onchainFees')} |
|||
// Dispatch
|
|||
setCryptoCurrency={setCryptoCurrency} |
|||
setFiatCurrency={setFiatCurrency} |
|||
/> |
|||
) |
|||
}, |
|||
options: { |
|||
decorator: story => <State store={store}>{story()}</State> |
|||
} |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}) |
|||
.addWithChapters('PayButtons', { |
|||
chapters: [ |
|||
{ |
|||
sections: [ |
|||
{ |
|||
title: 'Default', |
|||
sectionFn: () => <PayButtons previousStep={action('clicked')} /> |
|||
}, |
|||
{ |
|||
title: 'Disabled', |
|||
sectionFn: () => <PayButtons previousStep={action('clicked')} disabled /> |
|||
}, |
|||
{ |
|||
title: 'Processing', |
|||
sectionFn: () => <PayButtons previousStep={action('clicked')} processing /> |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}) |
@ -1 +1,6 @@ |
|||
import 'jest-styled-components' |
|||
|
|||
import { configure } from 'enzyme' |
|||
import Adapter from 'enzyme-adapter-react-16' |
|||
|
|||
configure({ adapter: new Adapter() }) |
|||
|
@ -0,0 +1,11 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
import toJSON from 'enzyme-to-json' |
|||
import { PayButtons } from 'components/Pay' |
|||
|
|||
describe('component.Form.PayButtons', () => { |
|||
it('should render correctly with default props', () => { |
|||
const wrapper = shallow(<PayButtons />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
@ -0,0 +1,25 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
import toJSON from 'enzyme-to-json' |
|||
import { PayHeader } from 'components/Pay' |
|||
|
|||
describe('component.Pay.PayHeader', () => { |
|||
describe('onchain', () => { |
|||
it('should render correctly', () => { |
|||
const wrapper = shallow(<PayHeader title="Send Bitcoin" type="onchain" />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
|||
describe('offchain', () => { |
|||
it('should render correctly', () => { |
|||
const wrapper = shallow(<PayHeader title="Send Bitcoin" type="offchain" />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
|||
describe('generic', () => { |
|||
it('should render correctly', () => { |
|||
const wrapper = shallow(<PayHeader title="Send Bitcoin" />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,45 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
import toJSON from 'enzyme-to-json' |
|||
import { PaySummaryLightning } from 'components/Pay' |
|||
|
|||
const props = { |
|||
currentTicker: { |
|||
USD: { |
|||
last: 6477.78 |
|||
}, |
|||
EUR: { |
|||
last: 5656.01 |
|||
}, |
|||
GBP: { |
|||
last: 5052.73 |
|||
} |
|||
}, |
|||
cryptoCurrency: 'btc', |
|||
cryptoCurrencyTicker: 'BTC', |
|||
cryptoCurrencies: [ |
|||
{ |
|||
key: 'btc', |
|||
name: 'BTC' |
|||
}, |
|||
{ |
|||
key: 'bits', |
|||
name: 'bits' |
|||
}, |
|||
{ |
|||
key: 'sats', |
|||
name: 'satoshis' |
|||
} |
|||
], |
|||
fiatCurrency: 'USD', |
|||
/* eslint-disable max-len */ |
|||
payReq: |
|||
'lntb100u1pdaxza7pp5x73t3j7xgvkzgcdvzgpdg74k4pn0uhwuxlxu9qssytjn77x7zs4qdqqcqzysxqyz5vqd20eaq5uferzgzwasu5te3pla7gv8tzk8gcdxlj7lpkygvfdwndhwtl3ezn9ltjejl3hsp36ps3z3e5pp4rzp2hgqjqql80ec3hyzucq4d9axl', |
|||
setCryptoCurrency: jest.fn() |
|||
} |
|||
describe('component.Form.PaySummaryLightning', () => { |
|||
it('should render correctly', () => { |
|||
const wrapper = shallow(<PaySummaryLightning {...props} />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
@ -0,0 +1,45 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
import toJSON from 'enzyme-to-json' |
|||
import { PaySummaryOnChain } from 'components/Pay' |
|||
|
|||
const props = { |
|||
amount: 1000, |
|||
address: 'mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c', |
|||
currentTicker: { |
|||
USD: { |
|||
last: 6477.78 |
|||
}, |
|||
EUR: { |
|||
last: 5656.01 |
|||
}, |
|||
GBP: { |
|||
last: 5052.73 |
|||
} |
|||
}, |
|||
cryptoCurrency: 'btc', |
|||
cryptoCurrencyTicker: 'BTC', |
|||
cryptoCurrencies: [ |
|||
{ |
|||
key: 'btc', |
|||
name: 'BTC' |
|||
}, |
|||
{ |
|||
key: 'bits', |
|||
name: 'bits' |
|||
}, |
|||
{ |
|||
key: 'sats', |
|||
name: 'satoshis' |
|||
} |
|||
], |
|||
fiatCurrency: 'USD', |
|||
setCryptoCurrency: jest.fn() |
|||
} |
|||
|
|||
describe('component.Form.PaySummaryOnchain', () => { |
|||
it('should render correctly', () => { |
|||
const wrapper = shallow(<PaySummaryOnChain {...props} />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
@ -0,0 +1,11 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
import toJSON from 'enzyme-to-json' |
|||
import { PaySummaryRow } from 'components/Pay' |
|||
|
|||
describe('component.Form.PaySummaryRow', () => { |
|||
it('should render correctly', () => { |
|||
const wrapper = shallow(<PaySummaryRow left="left contnet" right="right content" />) |
|||
expect(toJSON(wrapper)).toMatchSnapshot() |
|||
}) |
|||
}) |
@ -0,0 +1,54 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`component.Form.PayButtons should render correctly with default props 1`] = ` |
|||
<Styled(styled.div) |
|||
alignItems="center" |
|||
justifyContent="space-between" |
|||
> |
|||
<styled.div |
|||
width={0.2} |
|||
> |
|||
<Button |
|||
disabled={false} |
|||
onClick={[Function]} |
|||
processing={false} |
|||
px={0} |
|||
size="medium" |
|||
type="button" |
|||
variant="secondary" |
|||
> |
|||
<Styled(styled.div)> |
|||
<Styled(styled.div)> |
|||
<SvgBigArrowLeft /> |
|||
</Styled(styled.div)> |
|||
<Styled(styled.div) |
|||
ml={1} |
|||
> |
|||
<FormattedMessage |
|||
defaultMessage="Back" |
|||
id="components.Pay.back" |
|||
values={Object {}} |
|||
/> |
|||
</Styled(styled.div)> |
|||
</Styled(styled.div)> |
|||
</Button> |
|||
</styled.div> |
|||
<Button |
|||
disabled={false} |
|||
mx="auto" |
|||
processing={false} |
|||
size="medium" |
|||
type="submit" |
|||
variant="normal" |
|||
> |
|||
<FormattedMessage |
|||
defaultMessage="Next" |
|||
id="components.Pay.next" |
|||
values={Object {}} |
|||
/> |
|||
</Button> |
|||
<styled.div |
|||
width={0.2} |
|||
/> |
|||
</Styled(styled.div)> |
|||
`; |
@ -0,0 +1,96 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`component.Pay.PayHeader generic should render correctly 1`] = ` |
|||
<Text |
|||
textAlign="center" |
|||
> |
|||
<styled.div |
|||
css={ |
|||
Object { |
|||
"height": "55px", |
|||
} |
|||
} |
|||
mx="auto" |
|||
> |
|||
<SvgPaperPlane |
|||
height="35px" |
|||
width="35px" |
|||
/> |
|||
</styled.div> |
|||
<Heading1 |
|||
mx="auto" |
|||
> |
|||
Send Bitcoin |
|||
</Heading1> |
|||
<Heading4 |
|||
mx="auto" |
|||
> |
|||
|
|||
|
|||
</Heading4> |
|||
</Text> |
|||
`; |
|||
|
|||
exports[`component.Pay.PayHeader offchain should render correctly 1`] = ` |
|||
<Text |
|||
textAlign="center" |
|||
> |
|||
<styled.div |
|||
css={ |
|||
Object { |
|||
"height": "55px", |
|||
} |
|||
} |
|||
mx="auto" |
|||
> |
|||
<SvgLightning |
|||
height="45px" |
|||
width="45px" |
|||
/> |
|||
</styled.div> |
|||
<Heading1 |
|||
mx="auto" |
|||
> |
|||
Send Bitcoin |
|||
</Heading1> |
|||
<Heading4 |
|||
mx="auto" |
|||
> |
|||
|
|||
|
|||
Lightning Payment |
|||
</Heading4> |
|||
</Text> |
|||
`; |
|||
|
|||
exports[`component.Pay.PayHeader onchain should render correctly 1`] = ` |
|||
<Text |
|||
textAlign="center" |
|||
> |
|||
<styled.div |
|||
css={ |
|||
Object { |
|||
"height": "55px", |
|||
} |
|||
} |
|||
mx="auto" |
|||
> |
|||
<SvgOnchain |
|||
height="45px" |
|||
width="45px" |
|||
/> |
|||
</styled.div> |
|||
<Heading1 |
|||
mx="auto" |
|||
> |
|||
Send Bitcoin |
|||
</Heading1> |
|||
<Heading4 |
|||
mx="auto" |
|||
> |
|||
|
|||
On-Chain Payment |
|||
|
|||
</Heading4> |
|||
</Text> |
|||
`; |
@ -0,0 +1,129 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`component.Form.PaySummaryLightning should render correctly 1`] = ` |
|||
<Fragment> |
|||
<styled.div |
|||
pb={2} |
|||
> |
|||
<Styled(styled.div) |
|||
alignItems="center" |
|||
> |
|||
<styled.div |
|||
width={0.45454545454545453} |
|||
> |
|||
<Styled(styled.div) |
|||
alignItems="baseline" |
|||
flexWrap="wrap" |
|||
> |
|||
<styled.div> |
|||
<Text |
|||
fontSize={6} |
|||
textAlign="left" |
|||
> |
|||
<Value |
|||
currency="btc" |
|||
value={10000} |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
<WithTheme(Dropdown) |
|||
activeKey="btc" |
|||
items={ |
|||
Array [ |
|||
Object { |
|||
"key": "btc", |
|||
"name": "BTC", |
|||
}, |
|||
Object { |
|||
"key": "bits", |
|||
"name": "bits", |
|||
}, |
|||
Object { |
|||
"key": "sats", |
|||
"name": "satoshis", |
|||
}, |
|||
] |
|||
} |
|||
ml={2} |
|||
onChange={[MockFunction]} |
|||
/> |
|||
</Styled(styled.div)> |
|||
<Text |
|||
color="gray" |
|||
> |
|||
≈ |
|||
<FormattedNumber |
|||
currency="USD" |
|||
style="currency" |
|||
value={0.647778} |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
<styled.div |
|||
width={0.09090909090909091} |
|||
> |
|||
<Text |
|||
color="lightningOrange" |
|||
textAlign="center" |
|||
> |
|||
<SvgBigArrowRight |
|||
height="28px" |
|||
width="40px" |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
<styled.div |
|||
width={0.45454545454545453} |
|||
> |
|||
<Text |
|||
className="hint--bottom-left" |
|||
data-hint="03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463" |
|||
textAlign="right" |
|||
> |
|||
<Truncate |
|||
text="03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463" |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
</Styled(styled.div)> |
|||
</styled.div> |
|||
<Bar /> |
|||
<PaySummaryRow |
|||
left={ |
|||
<FormattedMessage |
|||
defaultMessage="Fee" |
|||
id="components.Pay.fee" |
|||
values={Object {}} |
|||
/> |
|||
} |
|||
right={ |
|||
<FormattedMessage |
|||
defaultMessage="unknown" |
|||
id="components.Pay.unknown" |
|||
values={Object {}} |
|||
/> |
|||
} |
|||
/> |
|||
<Bar /> |
|||
<PaySummaryRow |
|||
left={ |
|||
<FormattedMessage |
|||
defaultMessage="Total" |
|||
id="components.Pay.total" |
|||
values={Object {}} |
|||
/> |
|||
} |
|||
right={ |
|||
<React.Fragment> |
|||
<Value |
|||
currency="btc" |
|||
value={10000} |
|||
/> |
|||
|
|||
BTC |
|||
</React.Fragment> |
|||
} |
|||
/> |
|||
<Bar /> |
|||
</Fragment> |
|||
`; |
@ -0,0 +1,128 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`component.Form.PaySummaryOnchain should render correctly 1`] = ` |
|||
<Fragment> |
|||
<styled.div |
|||
pb={2} |
|||
> |
|||
<Styled(styled.div) |
|||
alignItems="center" |
|||
> |
|||
<styled.div |
|||
width={0.45454545454545453} |
|||
> |
|||
<Styled(styled.div) |
|||
alignItems="baseline" |
|||
flexWrap="wrap" |
|||
> |
|||
<styled.div> |
|||
<Text |
|||
fontSize={6} |
|||
textAlign="left" |
|||
> |
|||
<Value |
|||
currency="btc" |
|||
value={1000} |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
<WithTheme(Dropdown) |
|||
activeKey="btc" |
|||
items={ |
|||
Array [ |
|||
Object { |
|||
"key": "btc", |
|||
"name": "BTC", |
|||
}, |
|||
Object { |
|||
"key": "bits", |
|||
"name": "bits", |
|||
}, |
|||
Object { |
|||
"key": "sats", |
|||
"name": "satoshis", |
|||
}, |
|||
] |
|||
} |
|||
ml={2} |
|||
onChange={[MockFunction]} |
|||
/> |
|||
</Styled(styled.div)> |
|||
<Text |
|||
color="gray" |
|||
> |
|||
≈ |
|||
<FormattedNumber |
|||
currency="USD" |
|||
style="currency" |
|||
value={0.0647778} |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
<styled.div |
|||
width={0.09090909090909091} |
|||
> |
|||
<Text |
|||
color="lightningOrange" |
|||
textAlign="center" |
|||
> |
|||
<SvgBigArrowRight |
|||
height="28px" |
|||
width="40px" |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
<styled.div |
|||
width={0.45454545454545453} |
|||
> |
|||
<Text |
|||
className="hint--bottom-left" |
|||
data-hint="mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c" |
|||
textAlign="right" |
|||
> |
|||
<Truncate |
|||
text="mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c" |
|||
/> |
|||
</Text> |
|||
</styled.div> |
|||
</Styled(styled.div)> |
|||
</styled.div> |
|||
<Bar /> |
|||
<PaySummaryRow |
|||
left={ |
|||
<FormattedMessage |
|||
defaultMessage="Fee" |
|||
id="components.Pay.fee" |
|||
values={Object {}} |
|||
/> |
|||
} |
|||
right={ |
|||
<FormattedMessage |
|||
defaultMessage="unknown" |
|||
id="components.Pay.unknown" |
|||
values={Object {}} |
|||
/> |
|||
} |
|||
/> |
|||
<Bar /> |
|||
<PaySummaryRow |
|||
left={ |
|||
<FormattedMessage |
|||
defaultMessage="Total" |
|||
id="components.Pay.total" |
|||
values={Object {}} |
|||
/> |
|||
} |
|||
right={ |
|||
<React.Fragment> |
|||
<Value |
|||
currency="btc" |
|||
value={1000} |
|||
/> |
|||
|
|||
BTC |
|||
</React.Fragment> |
|||
} |
|||
/> |
|||
</Fragment> |
|||
`; |
@ -0,0 +1,30 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`component.Form.PaySummaryRow should render correctly 1`] = ` |
|||
<styled.div |
|||
py={2} |
|||
> |
|||
<Styled(styled.div) |
|||
alignItems="center" |
|||
> |
|||
<styled.div |
|||
width={0.5} |
|||
> |
|||
<Text |
|||
fontWeight="normal" |
|||
> |
|||
left contnet |
|||
</Text> |
|||
</styled.div> |
|||
<styled.div |
|||
width={0.5} |
|||
> |
|||
<Text |
|||
textAlign="right" |
|||
> |
|||
right content |
|||
</Text> |
|||
</styled.div> |
|||
</Styled(styled.div)> |
|||
</styled.div> |
|||
`; |
Loading…
Reference in new issue