Browse Source

feat(ui): add Pay components

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
parent
commit
fcafdec861
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 702
      app/components/Pay/Pay.js
  2. 74
      app/components/Pay/PayButtons.js
  3. 37
      app/components/Pay/PayHeader.js
  4. 163
      app/components/Pay/PaySummaryLightning.js
  5. 155
      app/components/Pay/PaySummaryOnChain.js
  6. 24
      app/components/Pay/PaySummaryRow.js
  7. 6
      app/components/Pay/index.js
  8. 27
      app/components/Pay/messages.js
  9. 1
      package.json
  10. 296
      stories/pages/pay.stories.js
  11. 5
      test/unit/__helpers__/setup-tests.js
  12. 11
      test/unit/components/Pay/PayButtons.spec.js
  13. 25
      test/unit/components/Pay/PayHeader.spec.js
  14. 45
      test/unit/components/Pay/PaySummaryLightning.spec.js
  15. 45
      test/unit/components/Pay/PaySummaryOnchain.spec.js
  16. 11
      test/unit/components/Pay/PaySummaryRow.spec.js
  17. 54
      test/unit/components/Pay/__snapshots__/PayButtons.spec.js.snap
  18. 96
      test/unit/components/Pay/__snapshots__/PayHeader.spec.js.snap
  19. 129
      test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap
  20. 128
      test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap
  21. 30
      test/unit/components/Pay/__snapshots__/PaySummaryRow.spec.js.snap
  22. 80
      yarn.lock

702
app/components/Pay/Pay.js

@ -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)

74
app/components/Pay/PayButtons.js

@ -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

37
app/components/Pay/PayHeader.js

@ -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">
&nbsp;
{type === 'onchain' && 'On-Chain Payment'} {type === 'offchain' && 'Lightning Payment'}
</Heading.h4>
</Text>
)
}
}
export default PayHeader

163
app/components/Pay/PaySummaryLightning.js

@ -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} />
&hellip;
</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

155
app/components/Pay/PaySummaryOnChain.js

@ -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} />
&hellip;
</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

24
app/components/Pay/PaySummaryRow.js

@ -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

6
app/components/Pay/index.js

@ -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'

27
app/components/Pay/messages.js

@ -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.'
})

1
package.json

@ -310,6 +310,7 @@
"@rebass/components": "^4.0.0-1",
"axios": "^0.18.0",
"bitcoinjs-lib": "^4.0.1",
"bolt11": "https://github.com/bitcoinjs/bolt11.git",
"connected-react-router": "^4.5.0",
"copy-to-clipboard": "^3.0.8",
"country-data-lookup": "^0.0.3",

296
stories/pages/pay.stories.js

@ -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 />
}
]
}
]
})

5
test/unit/__helpers__/setup-tests.js

@ -1 +1,6 @@
import 'jest-styled-components'
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() })

11
test/unit/components/Pay/PayButtons.spec.js

@ -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()
})
})

25
test/unit/components/Pay/PayHeader.spec.js

@ -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()
})
})
})

45
test/unit/components/Pay/PaySummaryLightning.spec.js

@ -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()
})
})

45
test/unit/components/Pay/PaySummaryOnchain.spec.js

@ -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()
})
})

11
test/unit/components/Pay/PaySummaryRow.spec.js

@ -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()
})
})

54
test/unit/components/Pay/__snapshots__/PayButtons.spec.js.snap

@ -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)>
`;

96
test/unit/components/Pay/__snapshots__/PayHeader.spec.js.snap

@ -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>
`;

129
test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap

@ -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>
`;

128
test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap

@ -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>
`;

30
test/unit/components/Pay/__snapshots__/PaySummaryRow.spec.js.snap

@ -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>
`;

80
yarn.lock

@ -3849,6 +3849,11 @@ big.js@^3.1.3:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
bigi@^1.1.0, bigi@^1.4.0:
version "1.4.2"
resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825"
integrity sha1-nGZalfiLiwj8Bc/XMfVhhZ1yWCU=
binary-extensions@^1.0.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14"
@ -3862,7 +3867,7 @@ binary@^0.3.0, binary@~0.3.0:
buffers "~0.1.1"
chainsaw "~0.1.0"
bindings@^1.3.0:
bindings@^1.2.1, bindings@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
integrity sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==
@ -3879,7 +3884,7 @@ bip32@^1.0.0:
typeforce "^1.11.5"
wif "^2.0.6"
bip66@^1.1.0:
bip66@^1.1.0, bip66@^1.1.3:
version "1.1.5"
resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"
integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=
@ -3891,6 +3896,27 @@ bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.0:
resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278"
integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==
bitcoinjs-lib@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-3.3.2.tgz#780c9c53ecb1222adb463b58bef26386067b609a"
integrity sha512-l5qqvbaK8wwtANPf6oEffykycg4383XgEYdia1rI7/JpGf1jfRWlOUCvx5TiTZS7kyIvY4j/UhIQ2urLsvGkzw==
dependencies:
bech32 "^1.1.2"
bigi "^1.4.0"
bip66 "^1.1.0"
bitcoin-ops "^1.3.0"
bs58check "^2.0.0"
create-hash "^1.1.0"
create-hmac "^1.1.3"
ecurve "^1.0.0"
merkle-lib "^2.0.10"
pushdata-bitcoin "^1.0.1"
randombytes "^2.0.1"
safe-buffer "^5.0.1"
typeforce "^1.11.3"
varuint-bitcoin "^1.0.4"
wif "^2.0.1"
bitcoinjs-lib@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-4.0.2.tgz#84fc9d14774c07581ad1795f2444706fc3f7a666"
@ -3944,7 +3970,7 @@ bluebird@~3.4.1:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0:
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.3, bn.js@^4.11.8, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
@ -3965,6 +3991,17 @@ body-parser@1.18.3:
raw-body "2.3.3"
type-is "~1.6.16"
"bolt11@https://github.com/bitcoinjs/bolt11.git":
version "1.0.0"
resolved "https://github.com/bitcoinjs/bolt11.git#d543a7aba5c3dd747627f523187abaf85976f19c"
dependencies:
bech32 "^1.1.2"
bitcoinjs-lib "^3.3.1"
bn.js "^4.11.8"
lodash "^4.17.4"
safe-buffer "^5.1.1"
secp256k1 "^3.4.0"
bonjour@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
@ -4068,7 +4105,7 @@ browser-resolve@^1.11.3:
dependencies:
resolve "1.1.7"
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
@ -6116,6 +6153,15 @@ downshift@^3.1.4:
prop-types "^15.6.0"
react-is "^16.5.2"
drbg.js@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b"
integrity sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=
dependencies:
browserify-aes "^1.0.6"
create-hash "^1.1.2"
create-hmac "^1.1.4"
duplexer2@~0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@ -6151,6 +6197,14 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecurve@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797"
integrity sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==
dependencies:
bigi "^1.1.0"
safe-buffer "^5.0.1"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -6292,7 +6346,7 @@ elegant-spinner@^1.0.1:
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
elliptic@^6.0.0, elliptic@^6.4.0:
elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0:
version "6.4.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a"
integrity sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==
@ -11219,7 +11273,7 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
nan@^2.10.0, nan@^2.9.2:
nan@^2.10.0, nan@^2.2.1, nan@^2.9.2:
version "2.11.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
@ -14496,6 +14550,20 @@ seamless-immutable@^7.1.2, seamless-immutable@^7.1.3:
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==
secp256k1@^3.4.0:
version "3.5.2"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.2.tgz#f95f952057310722184fe9c914e6b71281f2f2ae"
integrity sha512-iin3kojdybY6NArd+UFsoTuapOF7bnJNf2UbcWXaY3z+E1sJDipl60vtzB5hbO/uquBu7z0fd4VC4Irp+xoFVQ==
dependencies:
bindings "^1.2.1"
bip66 "^1.1.3"
bn.js "^4.11.3"
create-hash "^1.1.2"
drbg.js "^1.0.1"
elliptic "^6.2.3"
nan "^2.2.1"
safe-buffer "^5.1.0"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"

Loading…
Cancel
Save